diff --git a/.codacy.yaml b/.codacy.yaml new file mode 100644 index 000000000..9d602065c --- /dev/null +++ b/.codacy.yaml @@ -0,0 +1,13 @@ +--- +# 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. +# +# test/*.md are internal test-suite documents (e.g. the v2.0 routing +# regression gate at test/REGRESSION.md). Same rationale as specs/. +exclude_paths: + - 'specs/**' + - 'test/*.md' + - 'test/**/*.md' diff --git a/.codacy.yml b/.codacy.yml new file mode 100644 index 000000000..e01fa6d4c --- /dev/null +++ b/.codacy.yml @@ -0,0 +1,45 @@ +--- +# Codacy repository configuration. +# +# Every finding Codacy raised on the v2.0 work was triaged as a false positive +# or a cosmetic style nit; none was a genuine defect, and code-"fixing" most of +# them would have broken already-green structural gates (check-readme.sh / +# check-release-notes.sh assert byte-for-byte snippets and exact section/token +# counts) or the public API. +# +# Codacy's config file cannot enable/disable tools (that is dashboard-only) and +# is read from the repository's DEFAULT branch, so the only lever available here +# is exclude_paths — global, and per-engine for the tools whose findings on the +# library sources are all false positives. Globs are Java-glob: `dir/**` matches +# everything under dir; `**.ext` matches that extension anywhere. + +# Non-source trees are covered by their own purpose-built gates in +# .github/workflows/verify-build.yml (check-readme, check-release-notes, cpplint, +# the DR-008 stress/valgrind lanes, shellcheck-free CI helpers, etc.), not by +# Codacy's generic linters. Markdown prose, tests, examples, historical review +# specs and CI helper scripts produce only style/heuristic noise here. +exclude_paths: + - 'test/**' + - 'examples/**' + - 'docs/**' + - 'specs/**' + - 'scripts/**' + - '**.md' + +engines: + # cppcheck's unusedStructMember fires on public-API struct/class fields + # (features::tls, peer_address::bytes, the hook-context structs, ...) read by + # library *consumers*, not within the analyzed TU, and flags the correct + # unique_ptr::release() ownership transfer in webserver_websocket.cpp as an + # ignored return — all false positives. The authoritative cppcheck gate + # (cppcheck --error-exitcode=1 with the repo's own suppressions) runs green in + # GitHub Actions, so dropping Codacy's redundant copy over src/ loses nothing. + cppcheck: + exclude_paths: + - 'src/**' + # Flawfinder flags every strlen()/strncasecmp() on a \0-terminated char-array + # literal as a potential over-read (CWE-126 / CWE-120); in this codebase those + # arguments are always constexpr constants, so every hit is a false positive. + flawfinder: + exclude_paths: + - 'src/**' diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 000000000..b17361f3c --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +# These are supported funding model platforms + +github: [etr] diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..c121c157f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,56 @@ +--- +name: Bug report +about: Create a report to help us improve +title: "[BUG] Title" +labels: bug +assignees: etr + +--- + + + +### Prerequisites + +* [ ] Put an X between the brackets on this line if you have checked that your issue isn't already filed: https://github.com/search?l=&q=repo%3Aetr%2Flibhttpserver&type=Issues + +### Description + +[Description of the issue] + +### Steps to Reproduce + +1. [First Step] +2. [Second Step] +3. [and so on...] + +**Expected behavior:** [What you expect to happen] + +**Actual behavior:** [What actually happens] + +**Reproduces how often:** [What percentage of the time does it reproduce?] + +### Versions + +* OS version (if on linux, the output of "uname -a") +* libhttpserver version (please specify whether compiled or packaged) +* libmicrohttpd version (please specify whether compiled or packaged) + +If you have problems during build: +* Compiler version +* autotools version + +### Additional Information + +Any additional information, configuration (especially build configuration flags if you compiled the libraries) or data that might be necessary to reproduce the issue. + +If you have problems during build, please attach your config.log and the full scope of your error from make. + +If you have problems at execution, please: +* attach the stacktrace in case of crash (a coredump would be even better). +* provide a main that reproduces the error. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 000000000..a9833cf07 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,23 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: feature-request +assignees: etr + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe why the feature or enhancement you are proposing fits the library.** +A clear and concise explanation of the reason it fits into the library's mission. + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/PULL_REQUEST_TEMPLATE/bug_fix.md b/.github/PULL_REQUEST_TEMPLATE/bug_fix.md new file mode 100644 index 000000000..edcbbff71 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/bug_fix.md @@ -0,0 +1,59 @@ +### Requirements for Contributing a Bug Fix + +* Fill out the template below. Any pull request that does not include enough information to be reviewed in a timely manner may be closed at the maintainers' discretion. +* The pull request must only fix an existing bug. To contribute other changes, you must use a different template. You can see all templates at https://github.com/etr/libhttpserver/tree/master/.github/PULL_REQUEST_TEMPLATE. +* The pull request must update the test suite to demonstrate the changed functionality. +* After you create the pull request, all status checks must be pass before a maintainer reviews your contribution. For more details, please see https://github.com/etr/libhttpserver/tree/master/CONTRIBUTING.md#pull-requests. + +### Identify the Bug + + + +### Description of the Change + + + +### Alternate Designs + + + +### Possible Drawbacks + + + +### Verification Process + + + +### Release Notes + + diff --git a/.github/PULL_REQUEST_TEMPLATE/documentation.md b/.github/PULL_REQUEST_TEMPLATE/documentation.md new file mode 100644 index 000000000..2fc5a29ec --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/documentation.md @@ -0,0 +1,30 @@ +### Requirements for Contributing Documentation + +* Fill out the template below. Any pull request that does not include enough information to be reviewed in a timely manner may be closed at the maintainers' discretion. +* The pull request must only contribute documentation (for example, markdown files or API docs). To contribute other changes, you must use a different template. You can see all templates at https://github.com/etr/libhttpserver/tree/master/.github/PULL_REQUEST_TEMPLATE. + +### Description of the Change + + + +### Release Notes + + diff --git a/.github/PULL_REQUEST_TEMPLATE/feature_change.md b/.github/PULL_REQUEST_TEMPLATE/feature_change.md new file mode 100644 index 000000000..826d72cca --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/feature_change.md @@ -0,0 +1,62 @@ +### Requirements for Adding, Changing, or Removing a Feature + +* Fill out the template below. Any pull request that does not include enough information to be reviewed in a timely manner may be closed at the maintainers' discretion. +* The pull request must contribute a change that has been endorsed by the maintainer team. See details in the template below. +* The pull request must update the test suite to exercise the updated functionality. +* After you create the pull request, all status checks must be pass before a maintainer reviews your contribution. For more details, please see https://github.com/etr/libhttpserver/tree/master/CONTRIBUTING.md#pull-requests. + +### Issue or RFC Endorsed by Maintainers + + + +### Description of the Change + + + +### Alternate Designs + + + +### Possible Drawbacks + + + +### Verification Process + + + +### Release Notes + + diff --git a/.github/PULL_REQUEST_TEMPLATE/performance_improvement.md b/.github/PULL_REQUEST_TEMPLATE/performance_improvement.md new file mode 100644 index 000000000..82ea175fb --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/performance_improvement.md @@ -0,0 +1,55 @@ +### Requirements for Contributing a Performance Improvement + +* Fill out the template below. Any pull request that does not include enough information to be reviewed in a timely manner may be closed at the maintainers' discretion. +* The pull request must only affect performance of existing functionality. To contribute other changes, you must use a different template. You can see all templates at https://github.com/etr/libhttpserver/tree/master/.github/PULL_REQUEST_TEMPLATE. +* After you create the pull request, all status checks must be pass before a maintainer reviews your contribution. For more details, please see https://github.com/etr/libhttpserver/tree/master/CONTRIBUTING.md#pull-requests. + +### Description of the Change + + + +### Quantitative Performance Benefits + + + +### Possible Drawbacks + + + +### Verification Process + + + +### Applicable Issues + + + +### Release Notes + + diff --git a/.github/workflows/changelog-check.yml b/.github/workflows/changelog-check.yml new file mode 100644 index 000000000..632cedfb5 --- /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/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 000000000..fda437dbe --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,117 @@ +name: "CodeQL" + +# Supply-chain hardening (TASK-090): every action below is pinned to a full +# 40-hex commit SHA, mirroring the convention in verify-build.yml. The trailing +# `# vX.Y.Z` comment records the upstream release tag each SHA corresponds to, +# for rotation. +# +# Pinned actions and how to rotate them (read-only `gh` API; dereference the +# annotated tag to its commit SHA): +# actions/checkout v4.2.2 -> 11bd71901bbe5b1630ceea73d27597364c9af683 +# (kept identical to verify-build.yml so both workflows rotate together) +# gh api repos/actions/checkout/git/refs/tags/v4.2.2 --jq .object.sha +# github/codeql-action/* v3.36.3 -> 411c4c9a36b3fca4d674f06b6396b2c6d23522c6 +# init AND analyze ship from one repo/release and MUST share one SHA. +# SHA=$(gh api repos/github/codeql-action/git/refs/tags/v3.36.3 --jq .object.sha) +# gh api repos/github/codeql-action/git/tags/$SHA --jq .object.sha # deref annotated tag +# (@v1/@v2 of codeql-action are deprecated; v3 is the current major.) + +on: + push: + branches: [master] + pull_request: + # The branches below must be a subset of the branches above + branches: [master] + schedule: + - cron: '0 4 * * 4' + +# Least-privilege GITHUB_TOKEN: restrict to exactly what the CodeQL scan +# needs. `security-events: write` is required by github/codeql-action/analyze +# to upload the SARIF report to the security dashboard. `contents: read` is +# required by actions/checkout. `actions: read` is required on some GitHub +# plans/repository configurations to read the workflow run. Without an +# explicit block the token defaults to the repository's global setting. +permissions: + actions: read + contents: read + security-events: write + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + # Override automatic language detection by changing the below list + # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] + language: ['cpp'] + # Learn more... + # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection + + steps: + - name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + # We must fetch at least the immediate parents so that if this is + # a pull request then we can checkout the head. + fetch-depth: 2 + + # 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 + if: ${{ github.event_name == 'pull_request' }} + + - name: Fetch libmicrohttpd from cache + id: cache-libmicrohttpd + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + with: + path: libmicrohttpd-1.0.3 + key: ${{ runner.os }}-libmicrohttpd-1.0.3-codeql + + - name: Build libmicrohttpd dependency (if not cached) + # Supply-chain: libmicrohttpd-1.0.3.tar.gz is sha256-pinned. The digest + # must match the value used in verify-build.yml; both workflows track the + # same release. To rotate the libmicrohttpd version, update both the URL + # and the digest in the same PR (keep this file and verify-build.yml in + # sync). Obtain the new digest by downloading the tarball and running: + # sha256sum libmicrohttpd-.tar.gz + # Cross-check against the libmicrohttpd project's official release page. + # rotate: sha256 7816b57aae199cf5c3645e8770e1be5f0a4dfafbcb24b3772173dc4ee634126a + run: | + curl -fsSL https://s3.amazonaws.com/libhttpserver/libmicrohttpd_releases/libmicrohttpd-1.0.3.tar.gz -o libmicrohttpd-1.0.3.tar.gz ; + echo "7816b57aae199cf5c3645e8770e1be5f0a4dfafbcb24b3772173dc4ee634126a libmicrohttpd-1.0.3.tar.gz" | sha256sum -c ; + tar -xzf libmicrohttpd-1.0.3.tar.gz ; + cd libmicrohttpd-1.0.3 ; + ./configure --disable-examples ; + make ; + timeout-minutes: 30 + if: steps.cache-libmicrohttpd.outputs.cache-hit != 'true' + + - name: Install libmicrohttpd dependency + run: cd libmicrohttpd-1.0.3 ; sudo make install ; + timeout-minutes: 30 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@411c4c9a36b3fca4d674f06b6396b2c6d23522c6 # v3.36.3 + with: + languages: ${{ matrix.language }} + + # CodeQL's C/C++ database extractor traces the actual compile commands, so + # the library is built explicitly here rather than via the CodeQL Autobuild + # action (autobuild guesses the build and is intentionally not used). The + # build must be a clean from-scratch `make` so every TU is extracted; the + # in-source build (--enable-same-directory-build) keeps object files where + # the extractor expects them. + - name: Build the library (CodeQL manual mode) + run: | + ./bootstrap ; + ./configure --enable-same-directory-build ; + make ; + timeout-minutes: 30 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@411c4c9a36b3fca4d674f06b6396b2c6d23522c6 # v3.36.3 + timeout-minutes: 30 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..4681b92e0 --- /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-1.0.3 + key: ubuntu-latest-gcc-libmicrohttpd-1.0.3-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-1.0.3.tar.gz -o libmicrohttpd-1.0.3.tar.gz + tar -xzf libmicrohttpd-1.0.3.tar.gz + cd libmicrohttpd-1.0.3 + ./configure --disable-examples + make + + - name: Install libmicrohttpd + run: | + cd libmicrohttpd-1.0.3 + 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-1.0.3 + key: ubuntu-latest-gcc-libmicrohttpd-1.0.3-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-1.0.3.tar.gz -o libmicrohttpd-1.0.3.tar.gz + tar -xzf libmicrohttpd-1.0.3.tar.gz + cd libmicrohttpd-1.0.3 + ./configure --disable-examples + make + + - name: Install libmicrohttpd + run: | + cd libmicrohttpd-1.0.3 + 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-1.0.3 + key: macos-latest-gcc-libmicrohttpd-1.0.3-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-1.0.3.tar.gz -o libmicrohttpd-1.0.3.tar.gz + tar -xzf libmicrohttpd-1.0.3.tar.gz + cd libmicrohttpd-1.0.3 + ./configure --disable-examples + make + + - name: Install libmicrohttpd + run: | + cd libmicrohttpd-1.0.3 + 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-v3 + + - 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 --without-nghttp2 --without-libidn2 --without-libssh2 --without-brotli --without-zstd --without-librtmp --without-libpsl --disable-ldap --disable-ldaps + 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-1.0.3.tar.gz -o libmicrohttpd-1.0.3.tar.gz + tar -xzf libmicrohttpd-1.0.3.tar.gz + cd libmicrohttpd-1.0.3 + ./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/.github/workflows/verify-build.yml b/.github/workflows/verify-build.yml new file mode 100644 index 000000000..58672b315 --- /dev/null +++ b/.github/workflows/verify-build.yml @@ -0,0 +1,1429 @@ +name: "Verify Build" + +on: + push: + branches: [master] + pull_request: + # The branches below must be a subset of the branches above + branches: [master] + schedule: + - cron: '0 0 * * 0' + +# Restrict every job's GITHUB_TOKEN to read-only by default. +# Jobs that need broader access should override at the job level. +permissions: + contents: read + +jobs: + verify: + name: Verify + runs-on: ${{ matrix.os }} + env: + DEBUG: ${{ matrix.debug }} + COVERAGE: ${{ matrix.coverage }} + LINKING: ${{ matrix.linking }} + BUILD_TYPE: ${{ matrix.build-type }} + CC: ${{ matrix.c-compiler }} + CXX: ${{ matrix.cc-compiler }} + defaults: + run: + shell: ${{ matrix.shell }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest] + os-type: [ubuntu, mac] + test-group: [basic] + compiler-family: [none] + c-compiler: [gcc, clang] + cc-compiler: [g++, clang++] + debug: [debug, nodebug] + coverage: [coverage, nocoverage] + linking: [dynamic, static] + build-type: [classic] + shell: [bash] + exclude: + - os: ubuntu-latest + os-type: mac + - os: macos-latest + os-type: ubuntu + - c-compiler: gcc + cc-compiler: clang++ + - c-compiler: clang + cc-compiler: g++ + - c-compiler: clang + debug: debug + - debug: debug + coverage: nocoverage + - debug: nodebug + coverage: coverage + - linking: static + debug: debug + - linking: static + coverage: coverage + include: + - test-group: extra + os: ubuntu-latest + os-type: ubuntu + build-type: asan + compiler-family: clang + c-compiler: clang-18 + cc-compiler: clang++-18 + debug: debug + coverage: nocoverage + shell: bash + # TASK-087: restored msan lane. The previous entry (retired + # Ubuntu / Clang toolchain) was dropped when GitHub removed that + # runner image and never replaced, so MemorySanitizer went + # silently absent. This lane runs clang-18 against an + # instrumented libc++ + instrumented + # libmicrohttpd (both built and cached in-CI), with gnutls and + # curl kept out of the picture. See the "instrumented libc++" + # and "Run tests" steps below for how the false-positive-free + # dependency set is assembled and why coverage is scoped to the + # backend-free unit tests. + - test-group: extra + os: ubuntu-latest + os-type: ubuntu + build-type: msan + compiler-family: clang + c-compiler: clang-18 + cc-compiler: clang++-18 + debug: debug + coverage: nocoverage + shell: bash + - test-group: extra + os: ubuntu-latest + os-type: ubuntu + build-type: lsan + compiler-family: clang + c-compiler: clang-18 + cc-compiler: clang++-18 + debug: debug + coverage: nocoverage + shell: bash + - test-group: extra + os: ubuntu-latest + os-type: ubuntu + build-type: tsan + compiler-family: clang + c-compiler: clang-18 + cc-compiler: clang++-18 + debug: debug + coverage: nocoverage + shell: bash + - test-group: extra + os: ubuntu-latest + os-type: ubuntu + build-type: ubsan + compiler-family: clang + c-compiler: clang-18 + cc-compiler: clang++-18 + debug: debug + coverage: nocoverage + shell: bash + # gcc-9 and gcc-10 dropped (were here); gcc-11 is now the minimum GCC entry in the extra matrix. + # Both lack full C++20 library support (no concepts, no std::span, no ). + - test-group: extra + os: ubuntu-latest + os-type: ubuntu + build-type: none + compiler-family: gcc + c-compiler: gcc-11 + cc-compiler: g++-11 + debug: nodebug + coverage: nocoverage + shell: bash + - test-group: extra + os: ubuntu-latest + os-type: ubuntu + build-type: none + compiler-family: gcc + c-compiler: gcc-12 + cc-compiler: g++-12 + debug: nodebug + coverage: nocoverage + shell: bash + - test-group: extra + os: ubuntu-latest + os-type: ubuntu + build-type: none + compiler-family: gcc + c-compiler: gcc-13 + cc-compiler: g++-13 + debug: nodebug + coverage: nocoverage + shell: bash + - 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 + linking: dynamic + shell: bash + # clang-11, clang-12, clang-14, and clang-15 dropped (were here); clang-13 is now the minimum Clang entry. + # All four lack full C++20 support (concepts// gaps); clang-13 passes the autoconf C++20 check on ubuntu-22.04. + - test-group: extra + os: ubuntu-22.04 + os-type: ubuntu + build-type: none + compiler-family: clang + c-compiler: clang-13 + cc-compiler: clang++-13 + debug: nodebug + coverage: nocoverage + shell: bash + - test-group: extra + os: ubuntu-latest + os-type: ubuntu + build-type: none + compiler-family: clang + c-compiler: clang-16 + cc-compiler: clang++-16 + debug: nodebug + coverage: nocoverage + shell: bash + - test-group: extra + os: ubuntu-latest + os-type: ubuntu + build-type: none + compiler-family: clang + c-compiler: clang-17 + cc-compiler: clang++-17 + debug: nodebug + coverage: nocoverage + shell: bash + # TASK-088: split valgrind into three parallel matrix entries — one + # per tool (memcheck / helgrind / drd). GitHub Actions runs them + # concurrently, capping wall-clock cost at the slowest single tool + # instead of the sum of all three serially. + # + # The full check_PROGRAMS suite runs under each tool. helgrind/drd + # scope is intentionally NOT narrowed to unit/ tests only: the + # concurrency tests (threadsafety_stress, route_table_concurrency) + # are the primary reason for running race detectors (DR-008) and must + # not be subsetted out. A per-step timeout-minutes guards against a + # hung race-detector run (helgrind/drd can serialize busy-waits into + # apparent livelocks). + - test-group: extra + os: ubuntu-latest + os-type: ubuntu + build-type: valgrind-memcheck + compiler-family: gcc + c-compiler: gcc-14 + cc-compiler: g++-14 + debug: nodebug # configure step passes --enable-debug unconditionally for valgrind-* lanes + coverage: nocoverage + shell: bash + - test-group: extra + os: ubuntu-latest + os-type: ubuntu + build-type: valgrind-helgrind + compiler-family: gcc + c-compiler: gcc-14 + cc-compiler: g++-14 + debug: nodebug + coverage: nocoverage + shell: bash + - test-group: extra + os: ubuntu-latest + os-type: ubuntu + build-type: valgrind-drd + compiler-family: gcc + c-compiler: gcc-14 + cc-compiler: g++-14 + 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 + build-type: iwyu + compiler-family: clang + c-compiler: clang-18 + cc-compiler: clang++-18 + debug: nodebug + coverage: nocoverage + shell: bash + - test-group: performance + os: ubuntu-latest + os-type: ubuntu + build-type: select + compiler-family: gcc + c-compiler: gcc-14 + cc-compiler: g++-14 + debug: nodebug + coverage: nocoverage + shell: bash + - test-group: performance + os: ubuntu-latest + os-type: ubuntu + build-type: nodelay + compiler-family: gcc + c-compiler: gcc-14 + cc-compiler: g++-14 + debug: nodebug + coverage: nocoverage + shell: bash + - test-group: performance + os: ubuntu-latest + os-type: ubuntu + build-type: threads + compiler-family: gcc + c-compiler: gcc-14 + cc-compiler: g++-14 + debug: nodebug + coverage: nocoverage + shell: bash + - test-group: extra + os: ubuntu-latest + os-type: ubuntu + build-type: lint + compiler-family: gcc + 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. HEADER_HYGIENE_STRICT + # defaults to "yes" (flipped from "no" at TASK-020 close); any leak + # of forbidden backend headers through the umbrella fails CI loudly. + - 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 + # TASK-037 / PRD-FLG-REQ-001: flag-invariance-on gate. + # Compiles consumer_fixture with all features enabled (gnutls + full MHD). + - test-group: extra + os: ubuntu-latest + os-type: ubuntu + build-type: flag-invariance-on + compiler-family: gcc + c-compiler: gcc + cc-compiler: g++ + debug: nodebug + coverage: nocoverage + linking: dynamic + shell: bash + # TASK-037 / PRD-FLG-REQ-001: flag-invariance-off gate. + # Compiles consumer_fixture with MHD rebuilt without bauth/dauth/websockets + # and gnutls absent, so all four HAVE_* flags auto-detect to off. + - test-group: extra + os: ubuntu-latest + os-type: ubuntu + build-type: flag-invariance-off + compiler-family: gcc + c-compiler: gcc + cc-compiler: g++ + debug: nodebug + coverage: nocoverage + linking: dynamic + shell: bash + # TASK-076: TLS-no-CLI lane. Installs libgnutls28-dev (so the + # library compiles WITH HAVE_GNUTLS) but deliberately omits + # `gnutls-bin`, so the gnutls-cli binary is NOT on $PATH. The + # five `psk_connection_*` tests in test/integ/ws_start_stop.cpp + # must report `[SKIP]` (via LT_SKIP_IF(!has_gnutls_cli(), ...)) + # rather than tautological PASS. Post-test verification step + # below greps the test log for the [SKIP] markers. + - test-group: extra + os: ubuntu-latest + os-type: ubuntu + build-type: tls-no-cli + 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 + 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 + # 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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + # We must fetch at least the immediate parents so that if this is + # a pull request then we can checkout the head. + fetch-depth: 2 + + # 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@e9898307ac31d1a803454791be09ab9973336e1c # v2.31.1 + 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: | + # Invariance exclusion (doxygen) — TASK-090, documenting the gap the + # v2 audit flagged. + # WHAT: the doxygen zero-warning invariant (scripts/check-doxygen.sh, + # TASK-043) runs via `make check-local` on EVERY lane, but that + # script auto-SKIPs (exit 0) when doxygen is not on $PATH — see its + # `command -v doxygen` guard (scripts/check-doxygen.sh, the + # "SKIP — doxygen not installed" branch). doxygen + graphviz are + # intentionally NOT installed here, so the invariant silently no-ops + # on Windows. + # WHY excluded: `check-doxygen.sh` spawns `make doxygen-run`, and the + # autoconf-archive-generated MINGW recipe hangs indefinitely waiting + # on a cmd.exe batch prompt (the job timed out at the 6h CI ceiling). + # WHY it is safe: the invariant is fully enforced on the Linux lanes, + # where doxygen + graphviz ARE installed, so this is a deliberate + # per-platform exclusion, not a coverage gap. The Windows lane just + # builds and tests the library. + pacman --noconfirm -S --needed mingw-w64-x86_64-{toolchain,libtool,make,pkg-config,libsystre,gnutls,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 + # 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 apt-get update ; + if: ${{ matrix.os-type == 'ubuntu' }} + + - name: Install apache benchmark if needed + run: sudo apt-get install apache2-utils ; + if: ${{ matrix.test-group == 'performance' && matrix.os-type == 'ubuntu' }} + + - name: Install optional clang if needed + run: sudo apt-get install ${{ matrix.c-compiler }} + if: ${{ matrix.compiler-family == 'clang' && matrix.os-type == 'ubuntu' }} + + - name: Install optional gcc if needed + 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: ${{ startsWith(matrix.build-type, 'valgrind-') && matrix.os-type == 'ubuntu' }} + + - name: Install cpplint if needed + run: sudo pip3 install cpplint ; + if: ${{ matrix.build-type == 'lint' && matrix.os-type == 'ubuntu' }} + + - name: Install lizard if needed + run: sudo pip3 install lizard ; + if: ${{ matrix.build-type == 'lint' && matrix.os-type == 'ubuntu' }} + + - name: Install PMD (CPD) if needed + # CPD is shipped as part of the PMD distribution. The apt package + # is pinned to PMD 6.x; we want the modern PMD 7 CLI (`pmd cpd ...`) + # to match the script. Java is pre-installed on ubuntu-latest. + # + # Supply-chain: pmd.zip is sha256-pinned. To rotate PMD, update + # BOTH PMD_VERSION and PMD_SHA256 in the same PR. Obtain the new + # digest from the GitHub release page's pmd-dist--bin.zip + # asset (or `sha256sum` the freshly downloaded zip and record the + # resulting value in PMD_SHA256 in this file). + # + # For defence-in-depth, cross-check the digest against the + # pmd-dist--bin.zip.sha256 sidecar on the same release page. + # Alternatively, verify the Maven Central / OSSRH attestation for + # the same version if the GitHub release page itself may be + # compromised at rotation time. + run: | + set -euo pipefail + PMD_VERSION=7.24.0 + PMD_SHA256=110934b36d39c19094d1b77386931978093f238f2c2f1851748822b69c7367ac + curl -fsSL -o /tmp/pmd.zip \ + "https://github.com/pmd/pmd/releases/download/pmd_releases%2F${PMD_VERSION}/pmd-dist-${PMD_VERSION}-bin.zip" + echo "${PMD_SHA256} /tmp/pmd.zip" | sha256sum -c + sudo unzip -q /tmp/pmd.zip -d /opt + sudo ln -sf "/opt/pmd-bin-${PMD_VERSION}/bin/pmd" /usr/local/bin/pmd + pmd --version + if: ${{ matrix.build-type == 'lint' && matrix.os-type == 'ubuntu' }} + + - name: Install IWYU dependencies if needed + run: | + 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@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + with: + path: include-what-you-use + key: ${{ matrix.os }}-${{ matrix.c-compiler }}-include-what-you-use-pre-built + if: ${{ matrix.build-type == 'iwyu' && matrix.os-type == 'ubuntu' }} + + # Installing iwyu manually because clang and iwyu paths won't match on Ubuntu otherwise. + # Supply-chain: pinned to an immutable commit SHA on the upstream clang_18 + # branch (latest commit as of 2024-02-10, "[ci] Pin to LLVM 18"). To rotate, + # obtain the new SHA from https://github.com/include-what-you-use/include-what-you-use/commits/clang_18 + # and update IWYU_SHA below; the assertion will fail-fast on any mismatch. + - name: Build IWYU if requested + run: | + set -euo pipefail + IWYU_SHA=377eaef70cdda47368939f4d9beabfabe3f628f0 + 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 "${IWYU_SHA}" + HEAD_SHA="$(git rev-parse --verify HEAD)" + if [ "${HEAD_SHA}" != "${IWYU_SHA}" ]; then + echo "IWYU SHA mismatch: expected ${IWYU_SHA}, got ${HEAD_SHA}" >&2 + exit 1 + fi + echo "IWYU pinned at SHA: ${HEAD_SHA}" + 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++ ../ + make + sudo make install + if: ${{ matrix.build-type == 'iwyu' && matrix.os-type == 'ubuntu' && steps.cache-IWYU.outputs.cache-hit != 'true' }} + + - name: Install IWYU if requested + run: | + cd include-what-you-use/build_iwyu ; + sudo make install ; + if: ${{ matrix.build-type == 'iwyu' && matrix.os-type == 'ubuntu' }} + + - name: CURL from cache (for testing) + id: cache-CURL + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + with: + path: curl-7.75.0 + key: ${{ matrix.os }}-CURL-pre-built-v3 + if: ${{ matrix.os == 'macos-latest' }} + + - name: Build CURL (for testing) + # Supply-chain: curl-7.75.0.tar.gz is sha256-pinned. The expected digest + # is the official upstream value also published at https://curl.se/download/ + # (cross-checked against the libhttpserver S3 mirror). To rotate the curl + # version, update both CURL_VERSION and CURL_SHA256 in the same PR. + run: | + set -euo pipefail + CURL_VERSION=7.75.0 + CURL_SHA256=4d51346fe621624c3e4b9f86a8fd6f122a143820e17889f59c18f245d2d8e7a6 + curl -fsSL -o "curl-${CURL_VERSION}.tar.gz" \ + "https://libhttpserver.s3.amazonaws.com/travis_stuff/curl-${CURL_VERSION}.tar.gz" + echo "${CURL_SHA256} curl-${CURL_VERSION}.tar.gz" | shasum -a 256 -c + tar -xzf "curl-${CURL_VERSION}.tar.gz" + cd "curl-${CURL_VERSION}" + ./configure --with-darwinssl --without-ssl --without-nghttp2 --without-libidn2 --without-libssh2 --without-brotli --without-zstd --without-librtmp --without-libpsl --disable-ldap --disable-ldaps + make + if: ${{ matrix.os == 'macos-latest' && steps.cache-CURL.outputs.cache-hit != 'true' }} + + - name: Install CURL (for testing on mac only) + run: cd curl-7.75.0 ; sudo make install ; + if: ${{ matrix.os == 'macos-latest' }} + + - name: Install CURL (for testing on linux) + run: sudo apt-get install libcurl4-openssl-dev + if: ${{ matrix.os-type == 'ubuntu' }} + + - name: Install autotools on mac + run: brew install autoconf automake libtool + if: ${{ matrix.os == 'macos-latest' }} + + - name: Setup dependencies (only on linux) + run: | + sudo apt-get install info install-info ; + sudo apt-get install cppcheck ; + if: ${{ matrix.os-type == 'ubuntu' }} + + - name: Setup coverage dependencies (only on linux) + run: | + sudo pip install gcovr ; + if: ${{ matrix.os-type == 'ubuntu' && matrix.c-compiler == 'gcc' && matrix.debug == 'debug' && matrix.coverage == 'coverage' }} + + - name: Setup gnutls dependency (only on linux) + run: | + sudo apt-get install libgnutls28-dev gnutls-bin ; + # TASK-037: skip gnutls install on the flag-invariance-off lane so + # HAVE_GNUTLS auto-detects to off (the configure script just + # AC_CHECK_HEADER's gnutls/gnutls.h, no header == HAVE_GNUTLS=0). + # TASK-076: also skip on the tls-no-cli lane — it does its own + # install of libgnutls28-dev WITHOUT gnutls-bin in the next step + # so the per-test LT_SKIP_IF(!has_gnutls_cli(), ...) gate fires. + # TASK-087: also skip on the msan lane — installing gnutls would pull + # an uninstrumented gnutls/nettle/gmp tree into LDADD; with no header + # present configure auto-detects HAVE_GNUTLS=no and -lgnutls drops out. + if: ${{ matrix.os-type == 'ubuntu' && matrix.build-type != 'flag-invariance-off' && matrix.build-type != 'tls-no-cli' && matrix.build-type != 'msan' }} + + - name: Setup gnutls dependency WITHOUT gnutls-cli (TASK-076 tls-no-cli lane) + run: | + sudo apt-get install libgnutls28-dev ; + # Deliberate: NO gnutls-bin here. The lane asserts that + # has_gnutls_cli() returns false at runtime, which makes the + # five `psk_connection_*` tests in ws_start_stop.cpp emit + # `[SKIP]` rather than tautological PASS. + if which gnutls-cli >/dev/null 2>&1; then + echo "ERROR: gnutls-cli is on PATH but the tls-no-cli lane requires it absent" >&2 + exit 1 + fi + if: ${{ matrix.build-type == 'tls-no-cli' }} + + # TASK-087: MemorySanitizer reports use-of-uninitialized-value for any + # bytes written by uninstrumented code and later read by instrumented + # code. libc++ is on nearly every code path, so it must be instrumented + # too. Build LLVM libc++/libc++abi with MemoryWithOrigins once and cache + # the install prefix (keyed by os + compiler); rebuild only on a miss. + - name: Fetch instrumented libc++ from cache (msan lane) + id: cache-libcxx-msan + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + with: + path: ${{ github.workspace }}/libcxx-msan + key: ${{ matrix.os }}-${{ matrix.c-compiler }}-libcxx-msan-llvmorg-18.1.8-origins-v1 + if: ${{ matrix.build-type == 'msan' }} + + - name: Build instrumented libc++ (msan lane, if not cached) + # Supply-chain: llvmorg-18.1.8 is a mutable git tag, not an immutable + # commit SHA, so a post-clone SHA assertion guards against the tag + # being force-moved upstream (CWE-829). LLVM_SHA was obtained via + # `git ls-remote https://github.com/llvm/llvm-project refs/tags/llvmorg-18.1.8` + # at pin time; to rotate the tag, update both the --branch value below + # and LLVM_SHA in the same PR. Mirrors the IWYU SHA-pin pattern above. + run: | + set -euo pipefail + sudo apt-get install -y ninja-build cmake ; + LLVM_SHA=3b5b5c1ec4a3095ab096dd780e84d7ab81f3d7ff ; + git clone --depth 1 --branch llvmorg-18.1.8 https://github.com/llvm/llvm-project.git llvm-project ; + HEAD_SHA="$(git -C llvm-project rev-parse --verify HEAD)" ; + if [ "${HEAD_SHA}" != "${LLVM_SHA}" ]; then + echo "LLVM SHA mismatch: expected ${LLVM_SHA}, got ${HEAD_SHA}" >&2 ; + exit 1 ; + fi + echo "llvm-project pinned at SHA: ${HEAD_SHA}" ; + cmake -G Ninja -S llvm-project/runtimes -B build-libcxx-msan \ + -DCMAKE_C_COMPILER=clang-18 \ + -DCMAKE_CXX_COMPILER=clang++-18 \ + -DLLVM_ENABLE_RUNTIMES="libcxx;libcxxabi" \ + -DLIBCXXABI_USE_LLVM_UNWINDER=OFF \ + -DLLVM_USE_SANITIZER=MemoryWithOrigins \ + -DCMAKE_BUILD_TYPE=RelWithDebInfo \ + -DCMAKE_INSTALL_PREFIX="${{ github.workspace }}/libcxx-msan" ; + ninja -C build-libcxx-msan cxx cxxabi ; + ninja -C build-libcxx-msan install-cxx install-cxxabi ; + if: ${{ matrix.build-type == 'msan' && steps.cache-libcxx-msan.outputs.cache-hit != 'true' }} + + - name: Fetch libmicrohttpd from cache + id: cache-libmicrohttpd + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + with: + path: libmicrohttpd-1.0.3 + key: ${{ matrix.os }}-${{ matrix.c-compiler }}-libmicrohttpd-1.0.3-pre-built-v2 + # TASK-037: the flag-invariance-off lane rebuilds libmicrohttpd with + # every sub-feature disabled, so it must skip the stock cache fetch. + # The flag-invariance-on lane uses the stock build because + # libmicrohttpd 1.0.3 ships TLS/Basic/Digest auth on by default and + # does NOT ship the websocket extension at all. + # TASK-087: the msan lane rebuilds libmicrohttpd instrumented (so MHD + # output is not read back as uninitialized), so it skips this stock + # cache/build too. + if: ${{ matrix.os-type != 'windows' && matrix.build-type != 'no-dauth' && matrix.build-type != 'flag-invariance-off' && matrix.build-type != 'msan' && matrix.compiler-family != 'arm-cross' }} + + - name: Build libmicrohttpd dependency (if not cached) + run: | + curl https://s3.amazonaws.com/libhttpserver/libmicrohttpd_releases/libmicrohttpd-1.0.3.tar.gz -o libmicrohttpd-1.0.3.tar.gz ; + echo "7816b57aae199cf5c3645e8770e1be5f0a4dfafbcb24b3772173dc4ee634126a libmicrohttpd-1.0.3.tar.gz" | shasum -a 256 -c ; + tar -xzf libmicrohttpd-1.0.3.tar.gz ; + cd libmicrohttpd-1.0.3 ; + ./configure --disable-examples ; + make ; + # TASK-037: as above -- flag-invariance-off has its own dedicated + # libmicrohttpd build step below; flag-invariance-on uses the stock + # build because libmicrohttpd 1.0.3 ships no websocket extension. + # TASK-087: msan has its own dedicated instrumented build step below. + if: ${{ matrix.os-type != 'windows' && matrix.build-type != 'no-dauth' && matrix.build-type != 'flag-invariance-off' && matrix.build-type != 'msan' && matrix.compiler-family != 'arm-cross' && 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-1.0.3.tar.gz -o libmicrohttpd-1.0.3.tar.gz ; + echo "7816b57aae199cf5c3645e8770e1be5f0a4dfafbcb24b3772173dc4ee634126a libmicrohttpd-1.0.3.tar.gz" | shasum -a 256 -c ; + tar -xzf libmicrohttpd-1.0.3.tar.gz ; + cd libmicrohttpd-1.0.3 ; + ./configure --disable-examples --disable-dauth ; + make ; + if: ${{ matrix.build-type == 'no-dauth' }} + + - name: Fetch libmicrohttpd from cache (flag-invariance-off lane) + id: cache-libmicrohttpd-off + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + with: + path: libmicrohttpd-1.0.3 + key: ${{ matrix.os }}-${{ matrix.c-compiler }}-libmicrohttpd-1.0.3-flags-off-v1 + if: ${{ matrix.build-type == 'flag-invariance-off' }} + + - name: Build libmicrohttpd with all features off (TASK-037 flag-invariance-off lane) + run: | + curl https://s3.amazonaws.com/libhttpserver/libmicrohttpd_releases/libmicrohttpd-1.0.3.tar.gz -o libmicrohttpd-1.0.3.tar.gz ; + echo "7816b57aae199cf5c3645e8770e1be5f0a4dfafbcb24b3772173dc4ee634126a libmicrohttpd-1.0.3.tar.gz" | shasum -a 256 -c ; + tar -xzf libmicrohttpd-1.0.3.tar.gz ; + cd libmicrohttpd-1.0.3 ; + ./configure --disable-examples --disable-bauth --disable-dauth --disable-websockets ; + make ; + if: ${{ matrix.build-type == 'flag-invariance-off' && steps.cache-libmicrohttpd-off.outputs.cache-hit != 'true' }} + + - name: Fetch libmicrohttpd from cache (msan lane) + id: cache-libmicrohttpd-msan + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + # NOTE: the '-v1' suffix below must be bumped manually whenever the + # CFLAGS/LDFLAGS in the "Build instrumented libmicrohttpd" step change + # -- this key does not otherwise encode them, so a flag change alone + # would silently keep reusing a stale (differently-instrumented) + # cached artifact. + with: + path: libmicrohttpd-1.0.3 + key: ${{ matrix.os }}-${{ matrix.c-compiler }}-libmicrohttpd-1.0.3-msan-v1 + if: ${{ matrix.build-type == 'msan' }} + + - name: Build instrumented libmicrohttpd (TASK-087 msan lane) + run: | + curl https://s3.amazonaws.com/libhttpserver/libmicrohttpd_releases/libmicrohttpd-1.0.3.tar.gz -o libmicrohttpd-1.0.3.tar.gz ; + echo "7816b57aae199cf5c3645e8770e1be5f0a4dfafbcb24b3772173dc4ee634126a libmicrohttpd-1.0.3.tar.gz" | shasum -a 256 -c ; + tar -xzf libmicrohttpd-1.0.3.tar.gz ; + cd libmicrohttpd-1.0.3 ; + # Instrument MHD with MemorySanitizer so its output buffers, which + # libhttpserver reads on nearly every code path, are not flagged as + # uninitialized. --disable-https keeps gnutls (uninstrumented) out. + export CC=clang-18 ; + export CFLAGS='-fsanitize=memory -fsanitize-memory-track-origins -fno-omit-frame-pointer -g -O1' ; + export LDFLAGS='-fsanitize=memory' ; + ./configure --disable-examples --disable-https ; + make -j$(nproc) ; + if: ${{ matrix.build-type == 'msan' && steps.cache-libmicrohttpd-msan.outputs.cache-hit != 'true' }} + + - name: Install libmicrohttpd + run: cd libmicrohttpd-1.0.3 ; sudo make install ; + if: ${{ matrix.os-type != 'windows' && matrix.compiler-family != 'arm-cross' }} + + - 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: Verify libmicrohttpd built without bauth/dauth/websockets (TASK-037 flag-invariance-off lane) + # TASK-037 / PRD-FLG-REQ-001: belt-and-braces check that the bespoke + # libmicrohttpd build above actually stripped the symbols that drive + # libhttpserver's HAVE_BAUTH / HAVE_DAUTH / HAVE_WEBSOCKET + # auto-detection. If any of these slipped back in, the configure + # step would (correctly) flip the corresponding HAVE_* back on and + # the lane would no longer be exercising the flag-off invariant. + run: | + fail=0 ; + for sym in \ + MHD_basic_auth_get_username_password3 `# HAVE_BAUTH` \ + MHD_digest_auth_check3 `# HAVE_DAUTH` \ + MHD_queue_auth_fail_response `# HAVE_DAUTH (secondary)`; do + if nm /usr/local/lib/libmicrohttpd.so 2>/dev/null | grep -q "$sym"; then + echo "ERROR: $sym present in libmicrohttpd (expected absent)" ; + fail=1 ; + else + echo "Verified: $sym absent from libmicrohttpd" ; + fi ; + done ; + if ls /usr/local/lib/libmicrohttpd_ws* >/dev/null 2>&1; then # HAVE_WEBSOCKET + echo "ERROR: libmicrohttpd_ws present (expected absent)" ; + fail=1 ; + else + echo "Verified: libmicrohttpd_ws not built/installed" ; + fi ; + if [ "$fail" -ne 0 ]; then exit 1; fi + if: ${{ matrix.build-type == 'flag-invariance-off' }} + + - name: Build and install libmicrohttpd (Windows) + if: ${{ matrix.os-type == 'windows' }} + run: | + curl https://s3.amazonaws.com/libhttpserver/libmicrohttpd_releases/libmicrohttpd-1.0.3.tar.gz -o libmicrohttpd-1.0.3.tar.gz + echo "7816b57aae199cf5c3645e8770e1be5f0a4dfafbcb24b3772173dc4ee634126a libmicrohttpd-1.0.3.tar.gz" | shasum -a 256 -c + tar -xzf libmicrohttpd-1.0.3.tar.gz + cd libmicrohttpd-1.0.3 + ./configure --disable-examples --enable-poll=no + make + make install + + - name: Fetch libmicrohttpd from cache (ARM cross-compile) + id: cache-libmicrohttpd-arm + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + with: + path: libmicrohttpd-1.0.3-${{ matrix.build-type }} + key: ${{ matrix.os }}-${{ matrix.build-type }}-libmicrohttpd-1.0.3-cross-compiled + if: ${{ matrix.compiler-family == 'arm-cross' }} + + - name: Cross-compile libmicrohttpd for ARM + run: | + curl https://s3.amazonaws.com/libhttpserver/libmicrohttpd_releases/libmicrohttpd-1.0.3.tar.gz -o libmicrohttpd-1.0.3.tar.gz + echo "7816b57aae199cf5c3645e8770e1be5f0a4dfafbcb24b3772173dc4ee634126a libmicrohttpd-1.0.3.tar.gz" | shasum -a 256 -c + tar -xzf libmicrohttpd-1.0.3.tar.gz + mv libmicrohttpd-1.0.3 libmicrohttpd-1.0.3-${{ matrix.build-type }} + cd libmicrohttpd-1.0.3-${{ matrix.build-type }} + mkdir -p ${{ github.workspace }}/arm-sysroot + if [ "${{ matrix.build-type }}" = "arm32" ]; then + ./configure --host=arm-linux-gnueabihf --prefix=${{ github.workspace }}/arm-sysroot --disable-examples --disable-doc + else + ./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 from cache + run: | + cd libmicrohttpd-1.0.3-${{ matrix.build-type }} + 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 ; + if: ${{ matrix.os-type == 'ubuntu' }} + + - name: Run cpplint on code + run: cpplint --extensions=cpp,hpp --headers=hpp --recursive . ; + if: ${{ matrix.build-type == 'lint' && matrix.os-type == 'ubuntu' }} + + - name: Run per-function complexity gate (lizard) + run: ./scripts/check-complexity.sh + if: ${{ matrix.build-type == 'lint' && matrix.os-type == 'ubuntu' }} + + - name: Run copy/paste detection gate (PMD CPD) + run: ./scripts/check-duplication.sh + if: ${{ matrix.build-type == 'lint' && matrix.os-type == 'ubuntu' }} + + - name: Run per-file line-count gate + run: ./scripts/check-file-size.sh + if: ${{ matrix.build-type == 'lint' && matrix.os-type == 'ubuntu' }} + + - name: Run warning-suppression gate + # TASK-060: forbid file-scoped `#pragma GCC diagnostic ignored + # "-Warray-bounds"` (and any future broadened list) on the watched + # library TUs. A regression here would silently turn the warning + # off TU-wide. + run: ./scripts/check-warning-suppressions.sh + if: ${{ matrix.build-type == 'lint' && matrix.os-type == 'ubuntu' }} + + - name: Run skip-rationale gate + # TASK-077: forbid `#ifndef _WINDOWS`, `#ifndef DARWIN`, or + # `#if !defined(_WINDOWS)` blocks in `test/integ/` that lack a + # `// reason:` comment pointing at `test/PORTABILITY.md` (or a + # follow-up task). Without this gate, whole-suite skips silently + # drift on the Windows / Darwin CI lanes and the library's + # portability claim quietly decays. + run: ./scripts/check-skip-rationales.sh + if: ${{ matrix.build-type == 'lint' && matrix.os-type == 'ubuntu' }} + + - name: Run msan-lane structural gate + # TASK-087: asserts the msan CI lane is wired correctly (valid YAML, + # matrix entry present with mirrored keys, stale retired-OS commented + # block gone, MSAN_OPTIONS + scoped TESTS= wiring present). Mirrors the + # check-*.sh gate idiom used by complexity, duplication, file-size, + # warning-suppressions, and skip-rationales gates. + run: ./scripts/check-msan-lane.sh + if: ${{ matrix.build-type == 'lint' && matrix.os-type == 'ubuntu' }} + + - name: Run valgrind-lane structural gate + # TASK-088: asserts the Valgrind CI lane is wired correctly (valid YAML, + # valgrind matrix entry present, no --disable-valgrind-{helgrind,drd, + # sgcheck} flags remain, `make "check-valgrind-${TOOL}"` (per-tool + # AX_VALGRIND_CHECK target) invoked, and the results-print step + # surfaces helgrind + drd logs). Valgrind is + # Linux-only, so the actual race-detector run + finding-triage happen on + # the PR's Linux lane; this gate only guards the wiring. Mirrors the + # check-msan-lane.sh gate idiom. + run: ./scripts/check-valgrind-lane.sh + if: ${{ matrix.build-type == 'lint' && matrix.os-type == 'ubuntu' }} + + - name: Run DR-008 stress-lane structural gate + # TASK-092: asserts the two DR-008 concurrency/deadlock stress-gate + # steps (route_table_concurrency under TSan, stop()-from-handler + # deadlock contract) stay wired correctly -- present, correctly `if:` + # gated, and timeout-boxed. See test/PERFORMANCE.md ("CI wiring — + # DR-008 stress gates"). Mirrors the check-msan-lane.sh gate idiom. + run: ./scripts/check-dr008-lanes.sh + if: ${{ matrix.build-type == 'lint' && matrix.os-type == 'ubuntu' }} + + - name: Run CodeQL-workflow structural gate + # TASK-090: asserts codeql-analysis.yml stays hardened — every action + # SHA-pinned (no floating @vN), the dead Autobuild scaffolding gone, an + # explicit ./configure + make build present so CodeQL's C/C++ extractor + # traces the real compile, and codeql-action/init + analyze pinned to the + # SAME v3 SHA. To rotate the CodeQL SHA see the header comment in + # codeql-analysis.yml. Mirrors the check-valgrind-lane.sh gate idiom. + run: ./scripts/check-codeql-workflow.sh + if: ${{ matrix.build-type == 'lint' && matrix.os-type == 'ubuntu' }} + + - name: Run workflow-pinning / Codecov structural gate + # TASK-090: cross-workflow invariants — BOTH codeql-analysis.yml and + # verify-build.yml carry only SHA-pinned actions, the Codecov upload + # hard-fails CI (fail_ci_if_error: true, an UPLOAD failure — distinct + # from a coverage-percentage drop, which codecov.yml gates), and the + # Windows doxygen invariance exclusion stays documented. Mirrors the + # check-msan-lane.sh gate idiom. + run: ./scripts/check-workflow-pinning.sh + if: ${{ matrix.build-type == 'lint' && matrix.os-type == 'ubuntu' }} + + - name: Run libhttpserver configure + # TASK-038: fixed CXXLAGS -> CXXFLAGS typo so C++ TUs are instrumented by the sanitizer matrix. + run: | + # Set memory check flags. They need to stay in step as env variables don't propagate across steps. + if [ "$BUILD_TYPE" = "asan" ]; then export CFLAGS='-fsanitize=address'; export CXXFLAGS='-fsanitize=address'; export LDFLAGS='-fsanitize=address'; fi + # TASK-087: msan needs the instrumented libc++ built above, plus + # origins tracking and frame pointers for readable reports. Point + # the C++ TUs at the instrumented libc++ headers/libs and drop the + # default (uninstrumented) libstdc++ via -nostdinc++/-stdlib=libc++. + if [ "$BUILD_TYPE" = "msan" ]; then + LIBCXX_MSAN="${{ github.workspace }}/libcxx-msan" ; + export CFLAGS="-fsanitize=memory -fsanitize-memory-track-origins -fno-omit-frame-pointer -g -O1" ; + export CXXFLAGS="-stdlib=libc++ -nostdinc++ -isystem ${LIBCXX_MSAN}/include/c++/v1 -fsanitize=memory -fsanitize-memory-track-origins -fno-omit-frame-pointer -g -O1 -DLHS_SANITIZER_OWNS_OPERATOR_NEW" ; + export LDFLAGS="-stdlib=libc++ -fsanitize=memory -L${LIBCXX_MSAN}/lib -Wl,-rpath,${LIBCXX_MSAN}/lib -lc++abi" ; + fi + # tsan/msan/lsan runtimes define their own global operator new/delete; + # -DLHS_SANITIZER_OWNS_OPERATOR_NEW makes the arena alloc-pin test drop + # its overrides so it links (see test/unit/http_request_unescape_arena_test.cpp). + if [ "$BUILD_TYPE" = "lsan" ]; then export CFLAGS='-fsanitize=leak'; export CXXFLAGS='-fsanitize=leak -DLHS_SANITIZER_OWNS_OPERATOR_NEW'; export LDFLAGS='-fsanitize=leak'; fi + if [ "$BUILD_TYPE" = "tsan" ]; then export CFLAGS='-fsanitize=thread'; export CXXFLAGS='-fsanitize=thread -DLHS_SANITIZER_OWNS_OPERATOR_NEW'; export LDFLAGS='-fsanitize=thread'; fi + if [ "$BUILD_TYPE" = "ubsan" ]; then export CFLAGS='-fsanitize=undefined'; export CXXFLAGS='-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 + export CFLAGS='-mtune=generic' ; + export IPV6_TESTS_ENABLED="true" ; + fi + + ./bootstrap ; + 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 + ../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; + elif [ "$DEBUG" = "debug" ]; then + ../configure --enable-debug --disable-shared --disable-fastopen; + elif case "$BUILD_TYPE" in valgrind-*) true;; *) false;; esac; then + # TASK-088: Valgrind lanes (valgrind-memcheck / valgrind-helgrind / + # valgrind-drd). All three share the same configure flags. The race + # detectors (helgrind and drd) were previously turned off by per-tool + # disable flags; removing those flags re-enables them. Each lane + # invokes a single make check- target derived from BUILD_TYPE, + # so GitHub Actions runs memcheck, helgrind, and drd in parallel + # rather than serially. sgcheck (exp-sgcheck) stays off: removed + # upstream in Valgrind 3.20; ubuntu-latest ships 3.22; disabled in + # configure.ac via AX_VALGRIND_DFLT([sgcheck],[off]). + ../configure --enable-debug --disable-fastopen; + elif [ "$BUILD_TYPE" = "flag-invariance-on" ]; then + # TASK-037 / PRD-FLG-REQ-001: explicitly request all four feature + # flags so configure fails fast if any dependency is absent, rather + # than silently falling back to the feature-off path. + ../configure --disable-fastopen --enable-tls --enable-bauth --enable-dauth --enable-websocket; + elif [ "$BUILD_TYPE" = "iwyu" ]; then + ../configure --disable-examples; + else + ../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: Verify libhttpserver detected all features off (TASK-037 flag-invariance-off lane) + # TASK-037 / PRD-FLG-REQ-001 / arch §9 testing item 2. Asserts that + # configure auto-detected every HAVE_* feature as off in this lane; + # if any of them are 'yes', the lane wouldn't actually be exercising + # the flag-off invariant and we want the matrix to go red loudly. + run: | + cd build ; + # AC_MSG_NOTICE summary lines in config.log have two leading spaces + # rather than a 'configure:' prefix — the anchored pattern below + # matches that format exactly (prevents false-positive matches on + # unrelated config.log lines that contain the feature name). + fail=0 ; + for feat in "TLS Enabled" "Basic Auth" "Digest Auth" "WebSocket"; do + if grep -qE "^ ${feat}[[:space:]]*:[[:space:]]*no$" config.log; then + echo "Verified: ${feat} is off" ; + else + echo "ERROR: ${feat} is NOT off in this configuration" ; + grep "${feat}" config.log || echo "${feat} line not found" ; + fail=1 ; + fi ; + done ; + if [ "$fail" -ne 0 ]; then exit 1; fi + if: ${{ matrix.build-type == 'flag-invariance-off' }} + + - name: Verify libhttpserver detected all features on (TASK-037 flag-invariance-on lane) + # TASK-037 / PRD-FLG-REQ-001 / arch §9 testing item 2. Symmetric check + # to the off-lane step above: asserts configure auto-detected every + # HAVE_* feature as 'yes'. Without this, a misconfigured dependency + # (e.g. missing gnutls) would silently leave a feature off and the + # all-features-on invariant would not actually be exercised. + # Note: AC_MSG_NOTICE summary lines in config.log have two leading + # spaces rather than a 'configure:' prefix — the anchored pattern below + # matches that format exactly. + # + # WebSocket intentionally omitted: libmicrohttpd 1.0.3 (the pinned + # dependency version) does NOT ship microhttpd_ws.h in the standard + # tarball, so libhttpserver's HAVE_WEBSOCKET auto-detection cannot + # flip on with the upstream-supported build path. The off-lane + # explicitly asserts microhttpd_ws is absent; the on-lane covers + # the other three features whose detection IS exercisable on + # commodity CI runners. + run: | + cd build ; + fail=0 ; + for feat in "TLS Enabled" "Basic Auth" "Digest Auth"; do + if grep -qE "^ ${feat}[[:space:]]*:[[:space:]]*yes$" config.log; then + echo "Verified: ${feat} is on" ; + else + echo "ERROR: ${feat} is NOT on in this configuration" ; + grep "${feat}" config.log || echo "${feat} line not found" ; + fail=1 ; + fi ; + done ; + if [ "$fail" -ne 0 ]; then exit 1; fi + if: ${{ matrix.build-type == 'flag-invariance-on' }} + + - name: Print config.log + shell: bash + run: | + cd build ; + cat config.log ; + if: ${{ failure() }} + + # Make or run iwyu. If running iwyu, check for the result code to be 2 (IWYU always returns an error code, if it is 2, no corrections are necessary). + - name: Make or run IWYU + run: | + # 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++20 -DHTTPSERVER_COMPILATION -D_REENTRANT $CXXFLAGS" ; + } || { + if [ $? -ne 2 ]; then + return 1; + else + return 0; + fi + } + + return 0; + } + + cd build ; + if [ "$BUILD_TYPE" = "iwyu" ]; then + safe_make_iwyu ; + else + make ; + fi + + - name: Build consumer fixture (TASK-037 build-flag invariance gate) + # TASK-037 / PRD-FLG-REQ-001 / arch §9 testing item 2. The headline + # gate: the same test/consumer_fixture.cpp (which touches every + # previously HAVE_*-gated public symbol) must compile + link in + # both the all-features-on and all-features-off configurations. + # The matrix entries skip the broad `make check` / `cppcheck` steps + # below -- this single fixture compile + link is the build-flag- + # invariance assertion. + # make -C test consumer_fixture (builds in build/test/) invokes the + # subdirectory Makefile from within the build directory. + run: | + cd build ; + make -C test consumer_fixture ; + if: ${{ matrix.build-type == 'flag-invariance-on' || matrix.build-type == 'flag-invariance-off' }} + + - name: Run tests + run: | + cd build ; + # tsan: suppress libstdc++'s benign std::ctype narrow-cache race + # (see test/tsan.supp). All libhttpserver-internal races stay fatal. + if [ "$BUILD_TYPE" = "tsan" ]; then + export TSAN_OPTIONS="suppressions=$(pwd)/../test/tsan.supp" ; + make check ; + elif [ "$BUILD_TYPE" = "msan" ]; then + # TASK-087: msan coverage is deliberately scoped to the + # backend-free UNIT tests. curl (linked into every integ + # round-trip via test/integ/curl_helpers.hpp) cannot be + # practically instrumented, so its uninstrumented output would + # surface as false use-of-uninitialized-value reports. libc++ and + # libmicrohttpd ARE instrumented (see the steps above), so the + # unit subset — pure libhttpserver logic plus instrumented-MHD + # type inspection — runs clean without wildcard suppressions. + export LD_LIBRARY_PATH="${{ github.workspace }}/libcxx-msan/lib:${LD_LIBRARY_PATH:-}" ; + export MSAN_OPTIONS="abort_on_error=1" ; + # Auto-wire narrow suppressions only if triage produced the file. + # test/msan.supp is intentionally ABSENT until a real finding + # forces a narrow use-of-uninitialized-value: entry with + # an upstream-issue comment (never a wildcard over libhttpserver). + if [ -f ../test/msan.supp ]; then + export MSAN_OPTIONS="${MSAN_OPTIONS}:suppressions=$(pwd)/../test/msan.supp" ; + fi + # Derive the unit/ subset from the test Makefile so newly-added + # unit tests are covered automatically, then further exclude any + # unit/-sourced target whose test file transitively links curl + # (e.g. via curl_easy_perform() round-trips) -- curl cannot + # practically be msan-instrumented, so its uninstrumented output + # would surface as false use-of-uninitialized-value reports. + # NOTE: this only matches a target's _SOURCES entry when it is + # written on a single line (`_SOURCES = unit/.cpp`); + # a future multi-line _SOURCES assignment, or a unit/ target whose + # first source is not itself under unit/, would be silently + # dropped without failing CI (the empty-set guard below only + # catches total loss, not partial narrowing). + MSAN_TESTS="" ; + while IFS= read -r src_line; do + target="$(printf '%s' "$src_line" | sed -E 's/_SOURCES.*//')" ; + source_rel="$(printf '%s' "$src_line" | sed -E 's/^[A-Za-z0-9_]+_SOURCES[[:space:]]*=[[:space:]]*//')" ; + if grep -q 'curl/curl.h' "../test/${source_rel}" 2>/dev/null; then + echo "msan: excluding ${target} (${source_rel} includes curl/curl.h)" ; + continue ; + fi + MSAN_TESTS="${MSAN_TESTS}${target} " ; + done <<< "$(grep -E '^[A-Za-z0-9_]+_SOURCES[[:space:]]*=[[:space:]]*unit/' ../test/Makefile.am)" ; + if [ -z "$MSAN_TESTS" ]; then echo "ERROR: msan: derived empty test set — grep pattern found no unit/ targets in test/Makefile.am" >&2; exit 1; fi ; + make check TESTS="$MSAN_TESTS" ; + else + make check ; + fi + # TASK-037: flag-invariance-{on,off} are compile/link-only gates, so + # skip the full `make check` (which spins up real servers and would + # crash on the OFF lane for the BAUTH/DAUTH/WS unit tests anyway). + if: ${{ matrix.build-type != 'iwyu' && matrix.compiler-family != 'arm-cross' && matrix.build-type != 'header-hygiene' && matrix.build-type != 'flag-invariance-on' && matrix.build-type != 'flag-invariance-off' }} + + - name: Run route_table_concurrency under TSan (TASK-092) + # TASK-092: deliberate, time-boxed multi-iteration run of the + # DR-008 lock-order stress binary on the tsan lane. `make check` + # (above) already runs it once; this loops it RTC_ITERATIONS times + # so the tsan lane reliably hits the typical regression window. + # Reuses the same TSAN_OPTIONS suppression file as "Run tests" to + # mask the benign libstdc++ std::ctype narrow-cache race; any + # libhttpserver-internal race stays fatal. + timeout-minutes: 5 + run: | + cd build ; + export TSAN_OPTIONS="suppressions=$(pwd)/../test/tsan.supp" ; + make -C test check-route-table-concurrency ; + if: ${{ matrix.build-type == 'tsan' }} + + - name: Run stop()-from-handler deadlock contract (TASK-092) + # TASK-092: the DR-008 negative case — stop() from a handler thread + # makes libmicrohttpd self-join and abort. Gated behind + # HTTPSERVER_RUN_STOP_FROM_HANDLER=1 (set by check-stop-from-handler) + # so it is skipped by default in local runs. Runs on exactly one + # lane: the baseline Ubuntu gcc / nodebug / dynamic / classic job + # (os-type ubuntu excludes the identically-keyed Windows classic + # lanes). The documented failure mode is a hang -> timeout-minutes + # is the safety net; a clean zero-exit child would be a regression. + timeout-minutes: 3 + run: | + set -o pipefail ; + cd build ; + make -C test check-stop-from-handler 2>&1 | tee test/stop_from_handler.log ; + if: ${{ matrix.os-type == 'ubuntu' && matrix.test-group == 'basic' && matrix.c-compiler == 'gcc' && matrix.debug == 'nodebug' && matrix.linking == 'dynamic' && matrix.build-type == 'classic' }} + + - name: Verify stop()-from-handler contract fired (TASK-092) + # TASK-092: guards against Sub-test B + # (stop_from_handler_deadlocks_as_documented) silently reporting + # [SKIP] instead of exercising the DR-008 deadlock contract -- mirrors + # the "Verify [SKIP] markers fire on tls-no-cli lane (TASK-076)" + # grep-the-log pattern. + run: | + set -euo pipefail + cd build + LOG=test/stop_from_handler.log + if [ ! -f "$LOG" ]; then + echo "ERROR: $LOG missing — check-stop-from-handler did not run" >&2 + exit 1 + fi + if grep -q '^\[SKIP\] stop_from_handler_deadlocks_as_documented' "$LOG"; then + echo "ERROR: stop_from_handler_deadlocks_as_documented reported [SKIP] instead of exercising the deadlock contract" >&2 + cat "$LOG" + exit 1 + fi + if ! grep -q '^\[OK\] stop_from_handler' "$LOG"; then + echo "ERROR: expected an '[OK] stop_from_handler' marker in $LOG; none found" >&2 + cat "$LOG" + exit 1 + fi + echo "PASS — stop_from_handler_deadlocks_as_documented exercised the DR-008 deadlock contract" + if: ${{ matrix.os-type == 'ubuntu' && matrix.test-group == 'basic' && matrix.c-compiler == 'gcc' && matrix.debug == 'nodebug' && matrix.linking == 'dynamic' && matrix.build-type == 'classic' }} + + - 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 . + # HEADER_HYGIENE_STRICT defaults to "yes" (set at TASK-020 close); + # any leak of forbidden backend headers through the umbrella fails CI. + run: | + cd build + make check-hygiene + if: ${{ matrix.build-type == 'header-hygiene' }} + + - name: Verify [SKIP] markers fire on tls-no-cli lane (TASK-076) + # The five `psk_connection_*` tests in ws_start_stop.cpp gate on + # has_gnutls_cli(); since this lane installs libgnutls28-dev WITHOUT + # gnutls-bin, the gate must fire. The runner's stdout must contain + # at least 5 [SKIP] markers AND zero [CHECK FAILURE] / [ASSERT + # FAILURE] markers attributable to a psk_connection_* test. The + # ws_start_stop binary log is at build/test/ws_start_stop.log + # (Automake's per-test capture). + run: | + set -euo pipefail + cd build + LOG=test/ws_start_stop.log + if [ ! -f "$LOG" ]; then + echo "ERROR: $LOG missing — ws_start_stop did not run" >&2 + exit 1 + fi + SKIP_COUNT=$(grep -c '^\[SKIP\].*psk_connection' "$LOG" || true) + echo "Found $SKIP_COUNT [SKIP] markers attributable to gnutls-cli" + if [ "$SKIP_COUNT" -lt 5 ]; then + echo "ERROR: expected >=5 [SKIP] markers in $LOG; got $SKIP_COUNT" >&2 + cat "$LOG" + exit 1 + fi + if grep -E '\[(CHECK|ASSERT) FAILURE\].*psk_connection' "$LOG"; then + echo "ERROR: a psk_connection_* test reported FAILURE in tls-no-cli lane" >&2 + exit 1 + fi + if: ${{ matrix.build-type == 'tls-no-cli' }} + + - name: Verify TLS blocks exercised when gnutls-cli is present (TASK-076) + # Canary invariant on a representative healthy gnutls-bin-installed + # lane: the psk_connection_* tests must NOT report [SKIP]. If they + # do, gnutls-cli detection regressed. Runs on the classic + # ubuntu/gcc/nodebug/dynamic baseline only. + run: | + set -euo pipefail + cd build + LOG=test/ws_start_stop.log + if [ ! -f "$LOG" ]; then + echo "ERROR: $LOG missing — ws_start_stop did not run" >&2 + exit 1 + fi + if grep -E '^\[SKIP\].*psk_connection' "$LOG"; then + echo "ERROR: psk_connection_* skipped on a lane that has gnutls-bin" >&2 + cat "$LOG" + exit 1 + fi + echo "PASS — no psk_connection_* SKIPs on the with-gnutls baseline lane" + if: ${{ matrix.os-type == 'ubuntu' && matrix.test-group == 'basic' && matrix.compiler-family == 'gcc' && matrix.debug == 'nodebug' && matrix.linking == 'dynamic' && matrix.build-type == 'classic' }} + + - name: Print tests results + shell: bash + run: | + cd build ; + cat test/test-suite.log ; + if: ${{ failure() && matrix.build-type != 'iwyu' && matrix.compiler-family != 'arm-cross' && matrix.build-type != 'header-hygiene' }} + + - name: Verify Windows smoke test ran (TASK-077) + # TASK-077: assert the `ws_start_stop_suite::windows_smoke` test is + # actually compiled and executed on the native MinGW64 lane so the basic + # daemon-start-accept-one-GET path is observed end to end on Windows. + # Without this, a future build-flag tweak that inadvertently turns off the + # `#ifdef _WINDOWS` branch would silently erase the Windows round-trip + # coverage. Restricted to MINGW64: windows_smoke is gated on _WINDOWS + # (defined(_WIN32) && !defined(__CYGWIN__)) and does native-Windows setup + # (winsock2 / _WIN32_WINNT), so it is deliberately NOT built on the MSYS + # (Cygwin/POSIX) lane where _WIN32 is undefined. + shell: bash + run: | + cd build + if [ ! -f test/ws_start_stop.log ]; then + echo "ERROR: test/ws_start_stop.log is missing — windows_smoke could not be verified" >&2 + exit 1 + fi + if ! grep -E "windows_smoke" test/ws_start_stop.log; then + echo "ERROR: windows_smoke test did not appear in test/ws_start_stop.log" >&2 + exit 1 + fi + if: ${{ matrix.os-type == 'windows' && matrix.msys-env == 'MINGW64' }} + + - name: Run Valgrind checks + timeout-minutes: 90 + run: | + cd build ; + TOOL="${BUILD_TYPE#valgrind-}" ; + make "check-valgrind-${TOOL}" ; + if: ${{ startsWith(matrix.build-type, 'valgrind-') }} + + - name: Print Valgrind results + shell: bash + run: | + cd build ; + # TASK-088: surface memcheck + helgrind + drd logs. Without the + # helgrind/drd logs, race-detector failures would be invisible in the + # CI output, making finding-triage on the Linux lane blind. + for log in test-suite-memcheck.log test-suite-helgrind.log test-suite-drd.log; do + if [ -f "test/$log" ]; then + echo "===== test/$log =====" ; + cat "test/$log" ; + else + echo "test/$log not produced by this lane" ; + fi + done + if: ${{ always() && startsWith(matrix.build-type, 'valgrind-') }} + + - name: Run cppcheck + run: | + cd src/ ; + cppcheck --error-exitcode=1 . ; + # TASK-037: flag-invariance lanes are minimal gates -- skip cppcheck. + if: ${{ matrix.os-type == 'ubuntu' && matrix.compiler-family != 'arm-cross' && matrix.build-type != 'flag-invariance-on' && matrix.build-type != 'flag-invariance-off' }} + + - name: Run performance tests (select) + run: | + cd build + cd examples + ./benchmark_select 8080 $(nproc) & + sleep 5 && ab -n 1000000 -c 100 127.0.0.1:8080/plaintext + if: ${{ matrix.build-type == 'select' }} + + - name: Run performance tests (nodelay) + run: | + cd build + cd examples + ./benchmark_nodelay 8080 $(nproc) & + sleep 5 && ab -n 1000000 -c 100 127.0.0.1:8080/plaintext + if: ${{ matrix.build-type == 'nodelay' }} + + - name: Run performance tests (threads) + run: | + cd build + cd examples + ./benchmark_threads 8080 & + sleep 5 && ab -n 1000000 -c 100 127.0.0.1:8080/plaintext + if: ${{ matrix.build-type == 'threads' }} + + - name: Generate coverage report + run: | + cd build + 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 + uses: codecov/codecov-action@0fb7174895f61a3b6b78fc075e0cd60383518dac # v5.5.5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: build/coverage.xml + # TASK-090: hard-fail CI on a Codecov UPLOAD error (network / auth / + # tokenization / malformed report). Coverage integrity is a + # supply-chain + observability signal; a silently-dropped upload used + # to mask that the coverage number was stale or missing entirely. + # This is distinct from a coverage-PERCENTAGE drop, which is enforced + # separately by Codecov's own project/patch status checks driven by + # codecov.yml — not by this upload step. + fail_ci_if_error: true + if: ${{ matrix.os-type == 'ubuntu' && matrix.c-compiler == 'gcc' && matrix.debug == 'debug' && matrix.coverage == 'coverage' && success() }} diff --git a/.gitignore b/.gitignore index 72c98a64d..7b59faa6f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,11 @@ *.sw* *.*~ +*~ *.in *.php *.pm *.py +!scripts/lib/*.py *_wrap.* *.gcov *.gcno @@ -14,6 +16,8 @@ .idea libhttpserver.iml build/* +build-debug/ +_build/ aclocal.m4 autom4te.cache/ config.guess @@ -57,3 +61,9 @@ debian/rules redhat/libhttpserver.SPEC libhttpserver.pc libtool +.worktrees +.claude +CLAUDE.md +.groundwork-plans/ +.debug/ +.DS_Store diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 4020403d0..000000000 --- a/.travis.yml +++ /dev/null @@ -1,34 +0,0 @@ -language: cpp -compiler: - - gcc - - clang -env: - - DEBUG="debug" - - DEBUG="nodebug" - - LINKING="static" -before_install: - - sudo apt-get install info install-info - - sudo pip install cpp-coveralls - - sudo pip install gcovr - - wget ftp://ftp.gnu.org/gnu/libmicrohttpd/libmicrohttpd-0.9.52.tar.gz - - tar -xvzf libmicrohttpd-0.9.52.tar.gz - - cd libmicrohttpd-0.9.52 - - ./configure --prefix=/usr - - make - - sudo make install - - cd .. -script: - - ./bootstrap - - mkdir build - - cd build - - if [ $LINKING = "static" ]; then ../configure --enable-static --disable-fastopen; elif [ $DEBUG = "debug" ]; then ../configure --enable-debug --disable-shared --disable-fastopen; else ../configure --disable-fastopen; fi - - make - - make check -after_success: - - if [ $DEBUG = "debug" ]; then ../ci-report-coverage; fi -matrix: - exclude: - - compiler: clang - env: DEBUG="debug" - - compiler: clang - env: LINKING='static' diff --git a/AUTHORS b/AUTHORS index 6a68d8b72..ff66cfa2b 100644 --- a/AUTHORS +++ b/AUTHORS @@ -9,9 +9,17 @@ Craig Minihan Guo Xiao Philipp Claßen Vitaut Bajaryn +Felipe Zipitría +Steven 'Steve' Kendall +G. Mercat +Thomas Schätzlein +Matt van Niekerk +Jim Phillips (https://github.com/jcphill) +Ilya Brin (https://github.com/ilyabrin) - Support for building on MinGW/Cygwin systems Shane Peelar +Dean M. Sands, III - Support for building on MaxOsX Jan Klimke @@ -24,3 +32,25 @@ Marcel Pursche - Fixed error management and regex handling Julian Picht + +- Fix string termination when loading files in memory +martamoreton (Github: https://github.com/martamoreton) + +- Memory leaks +rdiazmartin + +- Cleanup of multiple parts of the code. Improved testing. Fixed issues with family urls. +bcsgh (Github: https://github.com/bcsgh) + +- Management of large uploads +Walter Landry +Jagat + +- Simplification of microhttpd dependency management +Christian Grothoff + +- Fixes to the behavior of "Method Not Allowed" and other cleanups. Fixed the behavior of the server when receiving invalid responses from libmicrohttpd. Fixed the file_response to handle not existing files and directories. +Alexander Dahl + +- General cleanup. Use of string_view. Support for handling of multiple arguments with the same name. +stuart-byma (https://github.com/stuart-byma) diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..565a76c1c --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,76 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, sex characteristics, gender identity and expression, +level of experience, education, socio-economic status, nationality, personal +appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at merlino.sebastiano@gmail.com. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see +https://www.contributor-covenant.org/faq diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..d923f747e --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,245 @@ +# Contributing to libhttpserver + +:+1::tada: First off, thanks for taking the time to contribute! :tada::+1: + +The following is a set of guidelines for contributing to libhttpserver. These are mostly guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request. + +#### Table Of Contents + +[Code of Conduct](#code-of-conduct) + +[I don't want to read this whole thing, I just have a question!!!](#i-dont-want-to-read-this-whole-thing-i-just-have-a-question) + +[How Can I Contribute?](#how-can-i-contribute) + * [Reporting Bugs](#reporting-bugs) + * [Suggesting Enhancements](#suggesting-enhancements) + * [Your First Code Contribution](#your-first-code-contribution) + * [Pull Requests](#pull-requests) + +[Styleguides](#styleguides) + * [Git Commit Messages](#git-commit-messages) + * [Documentation Styleguide](#documentation-styleguide) + +[Additional Notes](#additional-notes) + * [Issue and Pull Request Labels](#issue-and-pull-request-labels) + +## Code of Conduct + +This project and everyone participating in it is governed by the [Code of Conduct](CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. Please report unacceptable behavior to [merlino.sebastiano@gmail.com](mailto:merlino.sebastiano@gmail.com). + +## I don't want to read this whole thing I just have a question!!! + +> **Note:** Please don't file an issue to ask a question. You'll get faster results by using the resources below. + +We have an official community board where the community chimes in with helpful advice if you have questions. + +* [libhttpserver on Gitter](https://gitter.im/libhttpserver/community) + +## How Can I Contribute? + +### Reporting Bugs + +This section guides you through submitting a bug report for libhttpserver. Following these guidelines helps maintainers and the community understand your report :pencil:, reproduce the behavior :computer: :computer:, and find related reports :mag_right:. + +Before creating bug reports, please check [this list](#before-submitting-a-bug-report) as you might find out that you don't need to create one. When you are creating a bug report, please [include as many details as possible](#how-do-i-submit-a-good-bug-report). Fill out [the required template](https://github.com/etr/libhttpserver/blob/master/.github/ISSUE_TEMPLATE/bug_report.md), the information it asks for helps us resolve issues faster. + +> **Note:** If you find a **Closed** issue that seems like it is the same thing that you're experiencing, open a new issue and include a link to the original issue in the body of your new one. + +#### Before Submitting A Bug Report + +* **Try to debug the problem** You might be able to find the cause of the problem and fix things yourself. Most importantly, check if you can reproduce the problem in the latest version of libhttpserver (head on github). +* **Perform a [cursory search](https://github.com/search?l=&q=repo%3Aetr%2Flibhttpserver&type=Issues)** to see if the problem has already been reported. If it has **and the issue is still open**, add a comment to the existing issue instead of opening a new one. + +#### How Do I Submit A (Good) Bug Report? + +Bugs are tracked as [GitHub issues](https://guides.github.com/features/issues/). After you followed the steps above, create an issue and provide the following information by filling in [the template](https://github.com/etr/libhttpserver/blob/master/.github/ISSUE_TEMPLATE/bug_report.md). + +Explain the problem and include additional details to help maintainers reproduce the problem: + +* **Use a clear and descriptive title** for the issue to identify the problem. +* **Describe the exact steps which reproduce the problem** in as many details as possible. When listing steps, **don't just say what you did, but explain how you did it**. +* **Provide specific examples to demonstrate the steps**. Include links to files or GitHub projects, or copy/pasteable snippets, which you use in those examples. If you're providing snippets in the issue, use [Markdown code blocks](https://help.github.com/articles/markdown-basics/#multiple-lines). +* **Describe the behavior you observed after following the steps** and point out what exactly is the problem with that behavior. +* **Explain which behavior you expected instead and why.** +* **If you're reporting a crash**, include a crash report with a stack trace from the operating system. Include these in the issue in a [code block](https://help.github.com/articles/markdown-basics/#multiple-lines), a [file attachment](https://help.github.com/articles/file-attachments-on-issues-and-pull-requests/), or put it in a [gist](https://gist.github.com/) and provide link to that gist. +* **Consider attaching a simple snipped reproducing the problem. ** +* **If the problem is related to performance or memory**, include a CPU profile capture with your report. + +Provide more context by answering these questions: + +* **Did the problem start happening recently** (e.g. after updating to a new version of libhttpserver) or was this always a problem? +* If the problem started happening recently, **can you reproduce the problem in an older version of libhttpserver?** What's the most recent version in which the problem doesn't happen? You can download older versions of libhttpserver from [the releases page](https://github.com/etr/libhttpserver/releases). +* **Can you reliably reproduce the issue?** If not, provide details about how often the problem happens and under which conditions it normally happens. + +Include details about your configuration and environment: + +* **Which version of libhttpserver are you using?** +* **What's the name and version of the OS you're using (e.g. "uname -a" on linux) **? +* **What's the version of libmicrohttpd that you have installed? ** +* **Have you installed the libraries (both libhttpserver and libmicrohttpd) manually or through package manager? ** +* **Which options did you use when compiling? ** +* **What compiler version and version of autotools did you use? ** + +### Feature Requests and Enhancements + +This section guides you through submitting an enhancement suggestion for libhttpserver, including completely new features and minor improvements to existing functionality. Following these guidelines helps maintainers and the community understand your suggestion and find related suggestions. + +Before creating enhancement suggestions, please check [this list](#before-submitting-an-enhancement-suggestion) as you might find out that you don't need to create one. When you are creating an enhancement suggestion, please [include as many details as possible](#how-do-i-submit-a-good-enhancement-suggestion). Fill in [the template](https://github.com/etr/libhttpserver/blob/master/.github/ISSUE_TEMPLATE/feature_request.md). + +#### Before Submitting An Enhancement Suggestion or a Feature Request + +* **Perform a [cursory search](https://github.com/search?l=&q=repo%3Aetr%2Flibhttpserver&type=Issues)** to see if the enhancement has already been suggested. If it has, add a comment to the existing issue instead of opening a new one. + +#### How Do I Submit A (Good) Feature Request / Enhancement Suggestion? + +Enhancement suggestions are tracked as [GitHub issues](https://guides.github.com/features/issues/). Create an issue on that repository and provide the following information: + +* **Use a clear and descriptive title** for the issue to identify the suggestion. +* **Provide a step-by-step description of the suggested enhancement** in as many details as possible. +* **Provide a specific example to demonstrate the new feature**. +* **Describe the current behavior** and **explain which behavior you expected instead** and why. +* **Describe which alternatives you have considered**. +* **Explain why this enhancement would be useful** to most users and **why it fits the mission of the library**. + +### Your First Code Contribution + +Unsure where to begin contributing to libhttpserver? You can start by looking through these `beginner` and `help-wanted` issues: + +* [Beginner issues][beginner] - issues which should only require a few lines of code, and a test or two. +* [Help wanted issues][help-wanted] - issues which should be a bit more involved than `beginner` issues. + +Both issue lists are sorted by total number of comments. While not perfect, number of comments is a reasonable proxy for impact a given change will have. + +### Pull Requests + +The process described here has several goals: + +- Maintain libhttpserver's quality +- Fix problems that are important to users +- Engage the community in working toward the best possible solution +- Enable a sustainable system for maintainers to review contributions + +Please follow these steps to have your contribution considered by the maintainers: + +1. Follow all instructions in [the template](https://github.com/etr/libhttpserver/blob/master/PULL_REQUEST_TEMPLATE.md) +2. Follow the [styleguides](#styleguides) +3. After you submit your pull request, verify that all [status checks](https://help.github.com/articles/about-status-checks/) are passing
What if the status checks are failing?If a status check is failing, and you believe that the failure is unrelated to your change, please leave a comment on the pull request explaining why you believe the failure is unrelated. A maintainer will re-run the status check for you. If we conclude that the failure was a false positive, then we will open an issue to track that problem with our status check suite.
+ +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 + +* Limit the first line to 80 characters or less. +* Add a concise description of what your change does. +* Reference issues and pull requests liberally after the first line. + +### Documentation Styleguide + +* Use [Markdown](https://daringfireball.net/projects/markdown). + +## Additional Notes + +### Issue and Pull Request Labels + +This section lists the labels we use to help us track and manage issues and pull requests. + +[GitHub search](https://help.github.com/articles/searching-issues/) makes it easy to use labels for finding groups of issues or pull requests you're interested in. We encourage you to read about [other search filters](https://help.github.com/articles/searching-issues/) which will help you write more focused queries. + +The labels are loosely grouped by their purpose, but it's not required that every issue have a label from every group or that an issue can't have more than one label from the same group. + +Please open an issue on `etr/libhttpserver` if you have suggestions for new labels. + +#### Type of Issue and Issue State + +| Label name | `etr/libhttpserver` :mag_right: | Description | +| --- | --- | --- | +| `feature-request` | [search][search-libhttpserver-repo-label-feature-request] | Feature requests or enhancements. | +| `bug` | [search][search-libhttpserver-repo-label-bug] | Confirmed bugs or reports that are very likely to be bugs. | +| `question` | [search][search-libhttpserver-repo-label-question] | Questions more than bug reports or feature requests (e.g. how do I do X). | +| `feedback` | [search][search-libhttpserver-repo-label-feedback] | General feedback more than bug reports or feature requests. | +| `help-wanted` | [search][search-libhttpserver-repo-label-help-wanted] | The maintainer would appreciate help from the community in resolving these issues. | +| `beginner` | [search][search-libhttpserver-repo-label-beginner] | Less complex issues which would be good first issues to work on for users who want to contribute to libhttpserver. | +| `more-information-needed` | [search][search-libhttpserver-repo-label-more-information-needed] | More information needs to be collected about these problems or feature requests (e.g. steps to reproduce). | +| `needs-reproduction` | [search][search-libhttpserver-repo-label-needs-reproduction] | Likely bugs, but haven't been reliably reproduced. | +| `blocked` | [search][search-libhttpserver-repo-label-blocked] | Issues blocked on other issues. | +| `duplicate` | [search][search-libhttpserver-repo-label-duplicate] | Issues which are duplicates of other issues, i.e. they have been reported before. | +| `wontfix` | [search][search-libhttpserver-repo-label-wontfix] | The maintainers have decided not to fix these issues for now, either because they're working as intended or for some other reason. | +| `invalid` | [search][search-libhttpserver-repo-label-invalid] | Issues which aren't valid (e.g. user errors). | + +#### Topic Categories + +| Label name | `etr/libhttpserver` :mag_right: | Description | +| --- | --- | --- | +| `windows` | [search][search-libhttpserver-repo-label-windows] | Related to Windows. | +| `linux` | [search][search-libhttpserver-repo-label-linux] | Related to Linux. | +| `mac` | [search][search-libhttpserver-repo-label-mac] | Related to macOS. | +| `travis` | [search][search-libhttpserver-repo-label-travis] | Related to travis and CI in general. | +| `tests` | [search][search-libhttpserver-repo-label-tests] | Related to tests (add tests, fix tests, etc...). | +| `documentation` | [search][search-libhttpserver-repo-label-documentation] | Related to any type of documentation. | +| `performance` | [search][search-libhttpserver-repo-label-performance] | Related to performance. | +| `security` | [search][search-libhttpserver-repo-label-security] | Related to security. | +| `api` | [search][search-libhttpserver-repo-label-api] | Related to libhttpserver's public APIs. | +| `git` | [search][search-libhttpserver-repo-label-git] | Related to Git functionality (e.g. problems with gitignore files or with showing the correct file status). | + +#### Pull Request Labels + +| Label name | `etr/libhttpserver` :mag_right: | Description | +| --- | --- | --- | +| `work-in-progress` | [search][search-libhttpserver-repo-label-work-in-progress] | Pull requests which are still being worked on, more changes will follow. | +| `needs-review` | [search][search-libhttpserver-repo-label-needs-review] | Pull requests which need code review, and approval from maintainers. | +| `under-review` | [search][search-libhttpserver-repo-label-under-review] | Pull requests being reviewed by maintainers. | +| `requires-changes` | [search][search-libhttpserver-repo-label-requires-changes] | Pull requests which need to be updated based on review comments and then reviewed again. | +| `needs-testing` | [search][search-libhttpserver-repo-label-needs-testing] | Pull requests which need manual testing. | + +[search-libhttpserver-repo-label-feature-request]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aetr%23Flibhttpserver+label%3Afeature-request +[search-libhttpserver-repo-label-bug]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aetr%23Flibhttpserver+label%3Abug +[search-libhttpserver-repo-label-question]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aetr%23Flibhttpserver+label%3Aquestion +[search-libhttpserver-repo-label-feedback]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aetr%23Flibhttpserver+label%3Afeedback +[search-libhttpserver-repo-label-help-wanted]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aetr%23Flibhttpserver+label%3Ahelp-wanted +[search-libhttpserver-repo-label-beginner]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aetr%23Flibhttpserver+label%3Abeginner +[search-libhttpserver-repo-label-more-information-needed]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aetr%23Flibhttpserver+label%3Amore-information-needed +[search-libhttpserver-repo-label-needs-reproduction]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aetr%23Flibhttpserver+label%3Aneeds-reproduction +[search-libhttpserver-repo-label-triage-help-needed]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aetr%23Flibhttpserver+label%3Atriage-help-needed +[search-libhttpserver-repo-label-windows]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aetr%23Flibhttpserver+label%3Awindows +[search-libhttpserver-repo-label-linux]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aetr%23Flibhttpserver+label%3Alinux +[search-libhttpserver-repo-label-mac]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aetr%23Flibhttpserver+label%3Amac +[search-libhttpserver-repo-label-travis]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aetr%23Flibhttpserver+label%3Atravis +[search-libhttpserver-repo-label-tests]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aetr%23Flibhttpserver+label%3Atests +[search-libhttpserver-repo-label-documentation]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aetr%23Flibhttpserver+label%3Adocumentation +[search-libhttpserver-repo-label-performance]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aetr%23Flibhttpserver+label%3Aperformance +[search-libhttpserver-repo-label-security]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aetr%23Flibhttpserver+label%3Asecurity +[search-libhttpserver-repo-label-api]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aetr%23Flibhttpserver+label%3Aapi +[search-libhttpserver-repo-label-git]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aetr%23Flibhttpserver+label%3Agit +[search-libhttpserver-repo-label-blocked]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aetr%23Flibhttpserver+label%3Ablocked +[search-libhttpserver-repo-label-duplicate]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aetr%23Flibhttpserver+label%3Aduplicate +[search-libhttpserver-repo-label-wontfix]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aetr%23Flibhttpserver+label%3Awontfix +[search-libhttpserver-repo-label-invalid]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aetr%23Flibhttpserver+label%3Ainvalid +[search-libhttpserver-repo-label-build-error]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aetr%23Flibhttpserver+label%3Abuild-error +[search-libhttpserver-repo-label-installer]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aetr%23Flibhttpserver+label%3Ainstaller +[search-libhttpserver-repo-label-deprecation-help]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aetr%23Flibhttpserver+label%3Adeprecation-help +[search-libhttpserver-repo-label-work-in-progress]: https://github.com/search?q=is%3Aopen+is%3Apr+repo%3Aetr%23Flibhttpserver+label%3Awork-in-progress +[search-libhttpserver-repo-label-needs-review]: https://github.com/search?q=is%3Aopen+is%3Apr+repo%3Aetr%23Flibhttpserver+label%3Aneeds-review +[search-libhttpserver-repo-label-under-review]: https://github.com/search?q=is%3Aopen+is%3Apr+repo%3Aetr%23Flibhttpserver+label%3Aunder-review +[search-libhttpserver-repo-label-requires-changes]: https://github.com/search?q=is%3Aopen+is%3Apr+repo%3Aetr%23Flibhttpserver+label%3Arequires-changes +[search-libhttpserver-repo-label-needs-testing]: https://github.com/search?q=is%3Aopen+is%3Apr+repo%3Aetr%23Flibhttpserver+label%3Aneeds-testing + +[beginner]:https://github.com/search?utf8=%E2%9C%93&q=is%3Aopen+is%3Aissue+label%3Abeginner+label%3Ahelp-wanted+user%3Aetr+sort%3Acomments-desc +[help-wanted]:https://github.com/search?q=is%3Aopen+is%3Aissue+label%3Ahelp-wanted+user%3Aetr+sort%3Acomments-desc+-label%3Abeginner diff --git a/CPPLINT.cfg b/CPPLINT.cfg new file mode 100644 index 000000000..9add2a081 --- /dev/null +++ b/CPPLINT.cfg @@ -0,0 +1,4 @@ +linelength=200 +headers=hpp +extensions=cpp,hpp +filter=-test/littletest.hpp diff --git a/ChangeLog b/ChangeLog index be5666bcb..1d9d4b40b 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,242 +1,288 @@ -Wed Feb 26 21:31:00 2017 +0000 - Fixed problem with segfault when copying http_response object +Version 2.0.1 + + Fixed CI lint annotations for libcurl-required long types in the + raw multipart upload regression helper. + Fixed an unauthenticated server crash when a multipart file part omits + the name attribute. Unnamed file parts are ignored before memory + or disk upload handling. Thanks to AllenYen for the private report. + Corrected the documentation include path and graph font configuration. + Fixed release-note extraction on macOS and made the ABI packaging gate + use the configured patch version while retaining SONAME 2. + +Version 2.0.0 + + Fixed macOS CI failure when verifying the pinned libmicrohttpd + archive by using the portable `shasum -a 256` interface. + Replaced the blocking-start pthread mutex/condvar pair with C++20 + atomic wait/notify, avoiding a Valgrind 3.22 Helgrind stale + lock-address false positive, and suppressed the same third-party + detector limitation in libmicrohttpd-owned mutexes. + v2.0 release. See RELEASE_NOTES.md for the full v1->v2 porting summary. + SOVERSION bumped: shared library now ships as libhttpserver.so.2 + (Linux) / libhttpserver.2.dylib (Darwin). Parallel-installable + with any prior libhttpserver.so.0/1 on disk (runtime artefacts + only; dev-time .la/.a/.pc/headers are last-installer-wins by + design). + Added per-function cyclomatic complexity gate + (scripts/check-complexity.sh via `lizard`) and copy/paste + detection gate (scripts/check-duplication.sh via PMD CPD with + the C++ tokenizer), wired into the existing CI lint lane and + exposed locally as `make lint-complexity` / + `make lint-duplication`. Initial CCN ceiling intentionally set + above the current worst offender so CI stays green; ratcheted + down per refactor commit until reaching the long-term target + of 10. Duplication gate runs at PMD's default + minimum-tokens=100 from day one (currently zero hits). + +Version 0.20.0 + + Raised minimum C++ standard to C++20. Build now requires gcc >= 11 + 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. + Migrated Digest Auth to v3 API (MHD_digest_auth_check3, + MHD_digest_auth_check_digest3, MHD_queue_auth_required_response3) + with SHA-512/256 support, userhash, nonce binding, and structured + digest_auth_result enum. Default algorithm changed to SHA-256. + Added new response types: empty_response, pipe_response, iovec_response. + Added external event loop integration: webserver::run(), run_wait(), + get_fdset(), get_timeout(), add_connection(). + Added daemon management: quiesce(), get_listen_fd(), + get_active_connections(), get_bound_port(). + Added daemon options: listen_backlog, address_reuse, + connection_memory_increment, tcp_fastopen_queue_size, + sigpipe_handled_by_app, https_mem_dhparams, https_key_password, + https_priorities_append, no_alpn, client_discipline_level. + Added startup flags: no_listen_socket, no_thread_safety, turbo, + suppress_date_header. + Added WebSocket support (conditional on HAVE_WEBSOCKET): + websocket_handler, websocket_session, register_ws_resource(). + TASK-035: replaced the raw-pointer + register_ws_resource(string, websocket_handler*) overload with + smart-pointer overloads register_ws_resource(string, + unique_ptr) and register_ws_resource(string, + shared_ptr); added unregister_ws_resource(string). + Mirrors the TASK-023 register_resource ownership model and is + consistent with DR-010 / PRD-HDL-REQ-003 / PRD-HDL-REQ-005. + Duplicate registrations now throw std::invalid_argument + (previously a silent overwrite). On HAVE_WEBSOCKET-off builds + all three entry points throw feature_unavailable. + Added utility functions: reason_phrase(), is_feature_supported(), + get_mhd_version(). + 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. + 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. + Fixed std::terminate when MHD invokes the URI log callback with a + null uri pointer (e.g. port scans, half-open connections, or + non-HTTP traffic). Resolves issue #371. + Fixed std::terminate when MHD invokes the post-processor iterator with + a null key on a continuation chunk (a field split across multiple + callbacks). Resolves issue #375. + +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. + Enable error_log to handle params to interpolate in the string. + Reduced memory footprint of requests using of string_view. + Configurable management of upload files that allows to load files on disk only, memory only or both disk and memory. + Fixed error codes. + Method not allowed now returns 'allows' header. + Added method to access the gnutls_session struct. + Code cleanups. + Better use of RAII. + Improved test coverage. + Cleaned code to support cpplint and extra warnings. + Use pointers in place of non-const references. + Simplified dependency management for libmicrohttpd. + Added support on build for CodeQL security checks. + 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. + Updates to readme and documentation. + Slight performances improvement by allowing to skip a copy in + string_response constructor. + Moved windows builds to AppVeyor. + Made the library compatible with libmicrohttpd v0.9.71 and above. + +Version 0.18.1 - 2020-06-06 + + Prevent use of regex in http_endpoint outside of registration which could + allow DOS attacks. + +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. + 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. + +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. + 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. + The library now avoids collecting connection properties (headers, arguments, footers, cookies, etc...) unless explicitly asked by the client code. + Removed the support for integrated COMET logic. + Removed the support for caching logic. + Added integ tests. + Changed http_resource interface to use shared_ptr. + Improved interface of the http_response object. + Deprecated http_response_builder object. + +Version 0.16.0 - 2018-12-26 + + Fixed IPV6 parsing logic. + Added tests to support IP parsing, URL parsing and utilities. + +Version 0.15.0 - 2018-11-23 + + Solved problem with the server not being able to start on mac os. + +Version 0.14.0 - 2018-11-05 + + Moved http_endpoint as a sub-class of webserver. This avoids usage of friends. + +Version 0.13.0 - 2017-02-26 + + Fixed problem with segfault when copying http_response object. + +Version 0.12.0 - 2017-02-12 + + Updated to libmicrohttpd 0.9.52. + +Version 0.11.1 - 2016-07-13 -Wed Feb 12 13:14:01 2017 +0000 - Updated to libmicrohttpd 0.9.52 + Fixed problems with large payloads. + Fixed memory leak in http_response_ptr. -Wed Jul 13 02:23:11 2016 +0100 - Fixed problems with large payloads - Fixed memory leak in http_response_ptr +Version 0.11.0 - 2015-12-26 -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 + 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. -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.9.0 - 2015-04-15 -Sat Dec 26 15:08:22 2015 +0100 - Changed http_resource to use classic C++ polymorphism using virtual instead of CRTP + 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. -Fri Jul 17 21:38:54 2015 +0000 - Removed build dependency on pkg-config +Version 0.8.0 - 2014-07-23 -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 + Support for building on MinGW/Cygwin systems. + min libmicrohttpd version moved to 0.9.37. -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.7.2 - 2014-03-23 -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 + Continue the cleanup reducing webserver.cpp responsibilities. + Deep work on documentation. -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.7.0 - 2014-01-25 -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 + Cleaned-up webserver.cpp code to extract secondary classes. + Enforced immutability of webserver class. + Enabled library to compile on g++ 4.1.2. -Wed Oct 31 14:23:57 2012 +0100 - Changed dependency download method - Sebastiano Merlino +Version 0.5.4 - 2012-10-30 -Wed Oct 31 14:13:49 2012 +0100 - Added dependency to travis - 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. -Wed Oct 31 14:07:30 2012 +0100 - Changed travis build path - Sebastiano Merlino +Version 0.5.3 - 2012-10-30 -Wed Oct 31 14:02:59 2012 +0100 - Added travis conf to repo - Sebastiano Merlino + Added grow method to http_request. -Tue Oct 30 16:13:10 2012 +0100 - Changed the buggy debian changelog - Sebastiano Merlino +Version 0.5.2 - 2012-10-23 -Tue Oct 30 16:06:26 2012 +0100 - Changed version to v0.5.4 - Sebastiano Merlino + Changed default log behaviour to print nothing. + Added getters and setters for dynamic components of WS. -Tue Oct 30 15:59:45 2012 +0100 - Adjusted debian build rules - Sebastiano Merlino +Version 0.5.1 - 2012-10-10 -Tue Oct 30 12:52:04 2012 +0100 - Changed version to 0.5.3 - Added grow method to http_request - 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. -Tue Oct 23 12:46:48 2012 +0200 - Changed version from 0.5.1 to 0.5.2 - Sebastiano Merlino +Version 0.5.0 - 2012-10-05 -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 - -Mon Oct 22 12:13:11 2012 +0200 - Modified version number and changelog in order to prepare tag - Sebastiano Merlino - -Fri Oct 19 17:11:21 2012 +0200 - Added response constructor with byte - Sebastiano Merlino - -Mon Oct 15 11:16:22 2012 +0200 - Removed unuseful dependency from libuuid - Sebastiano Merlino - -Fri Oct 12 15:42:21 2012 +0200 - Solved a bug that made impossible to parse post data - Sebastiano Merlino - -Wed Oct 10 17:19:25 2012 +0200 - Moved to version 0.5.1 - Sebastiano Merlino - -Wed Oct 10 17:16:26 2012 +0200 - Added querystring to request attributes - Sebastiano Merlino - -Fri Oct 5 18:00:38 2012 +0200 - Merge branch 'master' of https://github.com/etr/libhttpserver - Conflicts: - src/webserver.cpp - Sebastiano Merlino - -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 + 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. -Sun Sep 23 19:09:54 2012 +0200 - Avoided the usage of the sole option MHD_USE_POLL - Sebastiano Merlino +Version 0.4.0 - 2012-08-26 -Thu Sep 20 08:47:24 2012 +0200 - Added forgotten modded_request.hpp file - Sebastiano Merlino - -Thu Sep 20 08:46:33 2012 +0200 - Added .gitignore file - Sebastiano Merlino - -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/INSTALL b/INSTALL deleted file mode 100644 index 97a254095..000000000 --- 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/Makefile.am b/Makefile.am index 4deb3bf82..ba6089eed 100644 --- a/Makefile.am +++ b/Makefile.am @@ -24,18 +24,593 @@ LIBTOOL_DEPS = @LIBTOOL_DEPS@ AUTOMAKE_OPTIONS = foreign 1.4 ACLOCAL_AMFLAGS = -I m4 -SUBDIRS = src test examples -DIST_SUBDIRS = src test examples -EXTRA_DIST = libhttpserver.pc.in $(DX_CONFIG) +SUBDIRS = src doc +DIST_SUBDIRS = src doc -MOSTLYCLEANFILES = $(DX_CLEANFILES) *.gcda *.gcno *.gcov +if !COND_CROSS_COMPILE +SUBDIRS += test +DIST_SUBDIRS += test + +if BUILD_EXAMPLES +SUBDIRS += examples +DIST_SUBDIRS += examples +endif + +endif + +EXTRA_DIST = libhttpserver.pc.in $(DX_CONFIG) scripts/extract-release-notes.sh scripts/test_extract_release_notes.sh scripts/validate-version.sh \ + scripts/check-examples.sh scripts/check-readme.sh scripts/check-release-notes.sh \ + scripts/check-doxygen.sh scripts/check-hooks-doc-spotcheck.sh \ + scripts/check-soversion.sh \ + scripts/check-msan-lane.sh scripts/check-valgrind-lane.sh \ + scripts/check-codeql-workflow.sh scripts/check-workflow-pinning.sh \ + scripts/test_check_codeql_workflow.sh scripts/test_check_workflow_pinning.sh \ + scripts/test_check_fence_balance.sh \ + scripts/lib/resolve-prefix.sh \ + scripts/lib/check-fence-balance.sh scripts/lib/v1-no-setters.txt \ + scripts/lib/find-matrix-includes.py \ + scripts/check-complexity.sh scripts/check-duplication.sh \ + scripts/check-file-size.sh \ + scripts/check-warning-suppressions.sh \ + scripts/test_check_warning_suppressions.sh \ + scripts/check-server-ready-helper.sh \ + scripts/test_check_server_ready_helper.sh \ + scripts/check-no-tautological-asserts.sh \ + scripts/check-deprecated-cookie-overload.sh \ + scripts/test_check_littletest_skip_exit_code.sh \ + scripts/verify-installed-examples.sh \ + test/headers/consumer_direct.cpp test/headers/consumer_detail.cpp test/headers/consumer_detail_connection_context.cpp \ + test/headers/consumer_umbrella.cpp \ + test/headers/consumer_post_umbrella.cpp \ + test/headers/consumer_umbrella_no_backend.cpp \ + LICENSE RELEASE_NOTES.md CONTRIBUTING.md CODE_OF_CONDUCT.md SECURITY.md + +# --------------------------------------------------------------------------- +# 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 omit CXXFLAGS, AM_CPPFLAGS, and per-target CPPFLAGS +# so that -DHTTPSERVER_COMPILATION (set per-directory in src/Makefile.am and +# test/Makefile.am AM_CPPFLAGS, not in configure.ac global CXXFLAGS) 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) +# Public-header gate (consumer_direct, consumer_post_umbrella): triggered when +# a public httpserver/* header is included without the umbrella having defined +# _HTTPSERVER_HPP_INSIDE_. +CHECK_HEADERS_PUBLIC_GATE_MSG = Only or can be included directly. +# Detail-header gate (consumer_detail, consumer_detail_connection_context): post-TASK-014 +# the detail-header gate is HTTPSERVER_COMPILATION-only and emits a message of +# the form "httpserver/detail/.hpp is internal; only include it when +# compiling libhttpserver (HTTPSERVER_COMPILATION must be defined)." +CHECK_HEADERS_DETAIL_GATE_MSG = is internal; only include it when compiling libhttpserver + +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_PUBLIC_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 ===" + @# Post-TASK-014: the detail-header gate is HTTPSERVER_COMPILATION-only. + @# consumer_detail.cpp defines _HTTPSERVER_HPP_INSIDE_ explicitly so this + @# check validates the stricter scenario (the macro alone must not satisfy + @# the gate). A.2 is distinct from A.1 because of the include-path + @# (detail/ vs public) and the different gate message. + @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_DETAIL_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.2b: direct detail-header (connection_context) include must fail ===" + @# Complements A.2 (http_endpoint.hpp); both detail headers must carry the gate. + @if $(CHECK_HEADERS_CXX) -c $(top_srcdir)/test/headers/consumer_detail_connection_context.cpp -o /dev/null 2>check-headers-A2b.log; then \ + echo "FAIL: consumer_detail_connection_context.cpp compiled but should have errored"; \ + cat check-headers-A2b.log; \ + rm -f check-headers-A2b.log; \ + exit 1; \ + fi + @if ! grep -q "$(CHECK_HEADERS_DETAIL_GATE_MSG)" check-headers-A2b.log; then \ + echo "FAIL: consumer_detail_connection_context.cpp failed but not for the gate reason"; \ + cat check-headers-A2b.log; \ + rm -f check-headers-A2b.log; \ + exit 1; \ + fi + @rm -f check-headers-A2b.log + @echo " PASS: A.2b gate fired as expected (connection_context.hpp)" + @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_PUBLIC_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, +# and that the httpserverpp symlink is present. +# This protects the public/private split as described in TASK-002 / DR-002. +# +# NOTE: when invoked standalone (without CHECK_INSTALL_SHARED=yes), this target +# runs `make install` internally, which requires the library to have been built +# first. If the library has not been compiled yet, the staged install will fail +# with a confusing error. Always run `make` before `make check-install-layout` +# when invoking standalone outside of `make check`. +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); \ + mkdir -p $(CHECK_INSTALL_STAGE); \ + $(MAKE) $(AM_MAKEFLAGS) install DESTDIR=$(CHECK_INSTALL_STAGE) >$(CHECK_INSTALL_STAGE)/install.log 2>&1 || { \ + echo "FAIL: staged install failed"; \ + cat $(CHECK_INSTALL_STAGE)/install.log; \ + rm -rf $(CHECK_INSTALL_STAGE); \ + exit 1; \ + }; \ + 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 + @# '*_impl.hpp' is the agreed suffix for PIMPL implementation headers (DR-002). + @# Any such file appearing in the installed tree indicates a Makefile regression. + @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 + @# Accept either a symlink (POSIX) or a regular file (MSYS2/mingw, where + @# `ln -s` falls back to a file copy unless MSYS=winsymlinks is set). + @# Either form satisfies the "includes #include resolve to + @# the umbrella" property the install hook is establishing. + @if ! test -e $(CHECK_INSTALL_STAGE)$(includedir)/httpserverpp; then \ + echo "FAIL: httpserverpp alias not installed under $(includedir)"; \ + 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-081 pthread-detector removal: is intentionally absent +# from the forbidden list below, AND the corresponding runtime-sentinel +# guards in test/unit/header_hygiene_test.cpp have been removed +# entirely. Both mainstream STLs (libc++ and libstdc++ in thread mode, +# which is the default whenever -D_REENTRANT is set, as configure.ac +# does) unconditionally pull in from any STL container +# header. The libhttpserver public surface uses STL containers +# (std::string, std::vector, std::map in http_request.hpp, http_resource.hpp, +# ip_representation.hpp, ...), so the pthread leak is structural to the +# C++ standard library on every CI lane libhttpserver supports. The +# detector could not be satisfied on any supported configuration without +# rewriting the public surface to drop STL containers, which would +# break source compatibility. See RELEASE_NOTES.md (Test infrastructure) +# for the deletion rationale. +# --------------------------------------------------------------------------- + +# NOTE: each entry matches a basename; the grep in check-hygiene anchors with a +# leading '/' so e.g. mypthread.h is not a false positive. Keep this list in +# sync with the #ifdef ladder in test/unit/header_hygiene_test.cpp. +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) +# Override on the command line to run against an in-flight umbrella: +# make check-hygiene HEADER_HYGIENE_STRICT=no +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 + +# NOTE: wildcard is evaluated at parse time; run `make clean` if new headers +# are added to src/httpserver/ in the same invocation that generates them. +# Makefile.am and consumer_umbrella_no_backend.cpp are also listed so that +# changes to HEADER_HYGIENE_FORBIDDEN or the consumer TU invalidate the stamp. +$(HYGIENE_STAMP): $(wildcard $(top_srcdir)/src/httpserver/*.hpp) \ + $(top_srcdir)/src/httpserver.hpp \ + $(top_srcdir)/Makefile.am \ + $(top_srcdir)/test/headers/consumer_umbrella_no_backend.cpp + @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; \ + if ! test -f "$(CHECK_HYGIENE_STAGE)$(includedir)/httpserver.hpp"; then \ + echo "FAIL: CHECK_HYGIENE_STAGE exists but does not contain $(includedir)/httpserver.hpp"; \ + echo " Was the staged install complete before invoking check-hygiene?"; \ + 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 | sed 's/.*"\(.*\)".*/\1/' | 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 the following static (no-install) checks first: +# check-headers, check-examples, check-readme, check-release-notes, +# check-doxygen, check-hooks-doc-spotcheck, lint-warning-suppressions +# Then 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 install-backed sub-checks can still be invoked standalone +# (they will do their own install when CHECK_*_SHARED is not set). +# +# CHECK_INSTALL_SHARED=yes / CHECK_HYGIENE_SHARED=yes: the caller has already +# staged the install; sub-check skips the install step to avoid double cost. +# This knob is set only by check-local; do not set it when invoking sub-checks +# standalone. +# The README / release-notes / hooks-doc / doxygen gates validate source-tree +# documentation: README.md links into specs/ and docs/architecture/, which are +# dev-facing and intentionally NOT shipped in the distributed tarball. Gate them +# on the presence of specs/ so that `make check` from an unpacked release tarball +# still runs the full test suite plus the header / example / lint gates, but +# skips the source-only doc gates whose inputs it cannot satisfy. +check-local: check-headers check-examples lint-warning-suppressions lint-server-ready-helper lint-server-ready-helper-selftest lint-no-tautological-asserts lint-deprecated-cookie-overload lint-littletest-skip-exit-code lint-fence-balance + @if test -d "$(top_srcdir)/specs"; then \ + $(MAKE) $(AM_MAKEFLAGS) check-readme check-release-notes check-doxygen check-hooks-doc-spotcheck; \ + else \ + echo "check-local: skipping README/release-notes/hooks-doc/doxygen gates (source-only; specs/ absent in distributed tarball)"; \ + fi + @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" + +# TASK-040: enforce static invariants on the two flagship examples +# (hello_world.cpp <= 10 LOC, lambda-only; shared_state.cpp class+mutex). +# Run as part of `make check` via check-local. +check-examples: + @echo "=== check-examples: enforce TASK-040 invariants on examples/ ===" + @$(top_srcdir)/scripts/check-examples.sh + +# TASK-041: enforce static invariants on README.md +# (byte-for-byte hello-world snippet, no v1 tokens, required sections, cross-links). +# Run as part of `make check` via check-local. +check-readme: + @echo "=== check-readme: enforce TASK-041 invariants on README.md ===" + @$(top_srcdir)/scripts/check-readme.sh + +# TASK-042: static checks on RELEASE_NOTES.md — see scripts/check-release-notes.sh +check-release-notes: + @$(top_srcdir)/scripts/test_extract_release_notes.sh + @echo "=== check-release-notes: enforce TASK-042 invariants on RELEASE_NOTES.md ===" + @$(top_srcdir)/scripts/check-release-notes.sh + +# TASK-043: run doxygen and fail on any substantive warning. The script +# delegates to `make doxygen-run` and parses stderr; environmental noise +# (obsolete config tags, missing graphviz) is filtered out. Skips with +# exit 0 if doxygen is not installed. +check-doxygen: + @echo "=== check-doxygen: enforce TASK-043 zero-warning invariant ===" + @BUILD_DIR="$(abs_top_builddir)" $(top_srcdir)/scripts/check-doxygen.sh + +# TASK-052: hook-bus documentation spotcheck (per-header @file/@brief blocks, +# eleven-phase enumeration on webserver::add_hook, five-permitted / +# six-rejected enumeration on http_resource::add_hook, alias callouts on the +# five v1 setters, hook-concurrency sentence in the class-level threading +# block). Static grep-based gate; complements check-doxygen's full Doxygen +# warning sweep. +check-hooks-doc-spotcheck: + @echo "=== check-hooks-doc-spotcheck: enforce TASK-052 invariants on hook public surface ===" + @$(top_srcdir)/scripts/check-hooks-doc-spotcheck.sh + +# TASK-044: SOVERSION acceptance gate. Stages a clean DESTDIR install and +# asserts that the v2.0 on-disk SONAME layout is correct (libhttpserver.so.2 +# on Linux / libhttpserver.2.dylib on Darwin, pkg-config --modversion 2.0.0, +# correct symlink chain). NOT wired into `make check` — it does its own +# `make install` and is intentionally invoked standalone to keep per-PR CI +# fast. Run manually before tagging a release. +check-soversion: + @echo "=== check-soversion: TASK-044 SOVERSION acceptance gate ===" + @BUILD_DIR="$(abs_top_builddir)" $(top_srcdir)/scripts/check-soversion.sh + +# Per-function cyclomatic complexity gate. Wraps `lizard` against src/ and +# fails if any function exceeds CCN_MAX (default in the script; ratcheted +# down as the v2.0 refactor lands each offender below the line). Not wired +# into `make check` — invoked directly by CI's `lint` lane to keep the +# gate visible as its own status. +lint-complexity: + @echo "=== lint-complexity: enforce per-function CCN ceiling ===" + @$(top_srcdir)/scripts/check-complexity.sh + +# Copy/paste detection gate. Wraps PMD's CPD with the C++ tokenizer +# against src/ and fails if any duplicate run is at or above +# CPD_MIN_TOKENS (default 100 in the script). Not wired into `make +# check` — invoked directly by CI's `lint` lane. +lint-duplication: + @echo "=== lint-duplication: enforce zero copy/paste duplication ===" + @$(top_srcdir)/scripts/check-duplication.sh + +# Per-file line-count gate. Fails if any src/ .cpp/.hpp exceeds +# FILE_LOC_MAX (default in the script; ratcheted down toward the +# long-term 500-line target as each offender is decomposed). Not wired +# into `make check` — invoked directly by CI's `lint` lane. +lint-file-size: + @echo "=== lint-file-size: enforce per-file line-count ceiling ===" + @$(top_srcdir)/scripts/check-file-size.sh + +# TASK-060: file-scoped warning-suppression gate. Fails if any .cpp +# file under src/ carries a top-level `#pragma GCC diagnostic ignored +# "-Warray-bounds"` that is not bracketed by a push/pop pair. Wired +# into `make check` via check-local (requires only bash/awk/grep, so +# runs in every CI lane and local developer builds without needing +# external tools). Also invoked directly by CI's `lint` lane. +lint-warning-suppressions: + @echo "=== lint-warning-suppressions: forbid file-scoped GCC warning suppressions ===" + @$(top_srcdir)/scripts/check-warning-suppressions.sh + +# TASK-075: server-ready helper gate. Fails if any +# test/integ/hooks_*.cpp file regresses to a bare +# `std::this_thread::sleep_for(...ms)` used as a server-ready wait +# instead of the shared `httpserver_test::wait_for_server_ready` +# helper from test/integ/server_ready.hpp. Wired into `make check` +# via check-local; pure bash/awk/grep, runs in every CI lane. +lint-server-ready-helper: + @echo "=== lint-server-ready-helper: forbid bare server-ready sleeps in hooks_* integ tests ===" + @$(top_srcdir)/scripts/check-server-ready-helper.sh + +# TASK-075: unit test for the server-ready helper gate itself. Runs the +# gate against synthetic pass/fail fixtures so a regression in the +# gate's detection logic is caught by `make check`, not just trusted. +# Pure bash/awk/grep, runs in every CI lane. +lint-server-ready-helper-selftest: + @echo "=== lint-server-ready-helper-selftest: self-test the server-ready gate ===" + @$(top_srcdir)/scripts/test_check_server_ready_helper.sh + +# TASK-076: forbid `LT_CHECK_EQ(1, 1)` in test/integ/ws_start_stop.cpp. +# Pre-TASK-076 ~33 catches in the TLS test suite used the tautological +# assertion as a fake "skip" — a build that silently lost TLS support +# would still report PASS. The replacement uses LT_SKIP_IF (added to +# test/littletest.hpp by TASK-076) which reports SKIP. This gate +# prevents regressing back to the broken pattern in ws_start_stop.cpp. +# Wired into `make check` via check-local; pure bash/grep, runs in +# every CI lane. +lint-no-tautological-asserts: + @echo "=== lint-no-tautological-asserts: forbid tautological pseudo-skip in ws_start_stop.cpp ===" + @$(top_srcdir)/scripts/check-no-tautological-asserts.sh + +# TASK-064 code review: negative-compile gate pinning the [[deprecated]] +# attribute on the legacy string-blob overload +# http_response::with_cookie(std::string, std::string). Compiles a TU +# calling the deprecated overload with -Werror=deprecated-declarations +# and asserts the compile FAILS (plus a positive control proving the +# structured overload builds cleanly). Complements the positive-side pin +# in test/unit/cookie_deprecation_sentinel_test.cpp. Wired into `make +# check` via check-local; needs only a C++ compiler on $PATH (no build +# tree or libmicrohttpd), so it runs in every lane. +lint-deprecated-cookie-overload: + @echo "=== lint-deprecated-cookie-overload: pin [[deprecated]] on legacy with_cookie(string, string) ===" + @CXX="$(CXX)" $(top_srcdir)/scripts/check-deprecated-cookie-overload.sh + +# TASK-076: pin the LT_SKIP exit-code semantics in test/littletest.hpp. +# Asserts (a) an all-skip binary exits 77 (Automake SKIP), (b) a mixed +# pass+skip binary exits 0 and emits both `[SKIP]` and `-> N skipped` +# stdout markers. The textual markers are what CI greps for in the +# tls-no-cli lane to assert per-test SKIPs fired. Wired into `make +# check` via check-local; needs only a C++ compiler on $PATH so it +# runs in every lane. +lint-littletest-skip-exit-code: + @echo "=== lint-littletest-skip-exit-code: pin LT_SKIP exit-code semantics ===" + @$(top_srcdir)/scripts/test_check_littletest_skip_exit_code.sh + +# TASK-091: pin the fence-balance state machine shared by check-readme and +# check-release-notes (scripts/lib/check-fence-balance.sh). Asserts that two +# consecutive opening fences (even ``` count) are rejected — the regression the +# old `count % 2` parity heuristic missed. Pure bash/awk, so it is wired into +# `make check` via check-local and runs in every lane. +lint-fence-balance: + @echo "=== lint-fence-balance: pin fenced-code-block open/close state machine ===" + @$(top_srcdir)/scripts/test_check_fence_balance.sh + +# TASK-087: structural gate asserting the msan CI lane in verify-build.yml +# stays wired (matrix entry present, stale commented-out block gone, +# MSAN_OPTIONS + scoped TESTS= wiring on the run step). Parses the workflow +# file, so — like lint-valgrind-lane — it is invoked directly by +# CI's `lint` lane rather than every check-local run. +lint-msan-lane: + @echo "=== lint-msan-lane: verify msan CI wiring ===" + @$(top_srcdir)/scripts/check-msan-lane.sh + +# TASK-088: structural gate asserting the valgrind CI lane in +# verify-build.yml stays wired (helgrind/drd matrix entries present, the +# --disable-valgrind-* configure flags gone, per-tool check-valgrind-* +# targets invoked, helgrind/drd logs surfaced). Parses the workflow file, so +# — like lint-msan-lane — it is invoked directly by CI's `lint` +# lane rather than every check-local run. +lint-valgrind-lane: + @echo "=== lint-valgrind-lane: verify valgrind CI wiring ===" + @$(top_srcdir)/scripts/check-valgrind-lane.sh + +# TASK-090: structural gate asserting codeql-analysis.yml stays hardened +# (every action SHA-pinned, Autobuild deleted, explicit ./configure + make +# build, init/analyze on one v3 SHA). The gate parses the workflow with +# PyYAML, so — like the msan/valgrind lane gates — it runs on +# CI's `lint` lane, not on every check-local run. The target also runs the +# gate's own fixture-driven unit test so `make lint-codeql-workflow` exercises +# the gate logic. +lint-codeql-workflow: + @echo "=== lint-codeql-workflow: self-test the gate, then verify CodeQL workflow wiring ===" + @$(top_srcdir)/scripts/test_check_codeql_workflow.sh + @$(top_srcdir)/scripts/check-codeql-workflow.sh + +# TASK-090: cross-workflow gate asserting BOTH workflows carry only SHA-pinned +# actions, the Codecov upload hard-fails CI (fail_ci_if_error: true), and the +# Windows doxygen invariance exclusion stays documented. PyYAML-parsing, so it +# runs on CI's `lint` lane. The target also runs the gate's unit test. +lint-workflow-pinning: + @echo "=== lint-workflow-pinning: self-test the gate, then verify pinning + Codecov wiring ===" + @$(top_srcdir)/scripts/test_check_workflow_pinning.sh + @$(top_srcdir)/scripts/check-workflow-pinning.sh + +.PHONY: check-headers check-install-layout check-hygiene check-local check-examples check-readme check-release-notes check-doxygen check-hooks-doc-spotcheck check-soversion lint-complexity lint-duplication lint-file-size lint-warning-suppressions lint-server-ready-helper lint-server-ready-helper-selftest lint-no-tautological-asserts lint-deprecated-cookie-overload lint-littletest-skip-exit-code lint-fence-balance lint-msan-lane lint-valgrind-lane lint-codeql-workflow lint-workflow-pinning + +# TASK-039: top-level convenience rule that descends into test/ to +# build and run the bench binaries (see test/Makefile.am and +# test/PERFORMANCE.md). NOT wired into `make check`; release-mode +# only. +bench: + $(MAKE) -C test bench + +.PHONY: bench + +MOSTLYCLEANFILES = $(DX_CLEANFILES) *.gcda *.gcno *.gcov \ + check-headers-A1.log check-headers-A2.log check-headers-A2b.log \ + check-headers-A3.log check-headers-A4.log \ + consumer_umbrella.check.o \ + check-hygiene.i check-hygiene.err check-hygiene-install.log \ + check-shared-install.log 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 cmakemoduledir = $(datadir)/cmake/Modules -cmakemodule_DATA = cmakemodule/FindLibHttpServer.cmake +dist_cmakemodule_DATA = cmakemodule/FindLibHttpServer.cmake include $(top_srcdir)/aminclude.am diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 000000000..a4d74934e --- /dev/null +++ b/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,10 @@ +Hello there! Welcome. Please follow the steps below to tell us about your contribution. + +1. Copy the correct template for your contribution + - Are you fixing a bug? Copy the template from https://raw.githubusercontent.com/etr/libhttpserver/master/.github/PULL_REQUEST_TEMPLATE/bug_fix.md + - Are you improving performance? Copy the template https://raw.githubusercontent.com/etr/libhttpserver/master/.github/PULL_REQUEST_TEMPLATE/performance_improvement.md + - Are you updating documentation? Copy the template from https://raw.githubusercontent.com/etr/libhttpserver/master/.github/PULL_REQUEST_TEMPLATE/documentation.md + - Are you changing functionality? Copy the template from https://raw.githubusercontent.com/etr/libhttpserver/master/.github/PULL_REQUEST_TEMPLATE/feature_change.md +2. Replace this text with the contents of the template +3. Fill in all sections of the template +4. Click "Create pull request" diff --git a/README.CentOS-7 b/README.CentOS-7 new file mode 100644 index 000000000..eb7728ff9 --- /dev/null +++ b/README.CentOS-7 @@ -0,0 +1,8 @@ +## Cent OS 7 / RHEL 7 + +CentOS 7's stock gcc (4.8.7) is far too old: this library requires a C++20 compiler +(gcc >= 11 or clang >= 13). + +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.FreeBSD b/README.FreeBSD new file mode 100644 index 000000000..853c55890 --- /dev/null +++ b/README.FreeBSD @@ -0,0 +1,9 @@ +## Building on FreeBSD (Tested on 12.0) + +# Due to the differences in the directory structures on BSD systems some minor tweaks need to occur +# Also, FreeBSD and AIX "make" command is not compatible with gmake, like Linux and Mingw are + +export MAKE=gmake +bootstrap +configure CPPFLAGS=-I/usr/local/include LDFLAGS=-L/usr/local/lib +gmake diff --git a/README.md b/README.md index 1bd46b181..3e16c34f5 100644 --- a/README.md +++ b/README.md @@ -1,1130 +1,1952 @@ -The libhttpserver (0.8.0) reference manual -========================================== - -[![Build Status](https://travis-ci.org/etr/libhttpserver.png?branch=master)](https://travis-ci.org/etr/libhttpserver) - -This library has been originally developed under the zencoders flags and this community has always supported me all along this work so I am happy to put the logo on this readme. - - When you see this tree, know that you've came across ZenCoders.org - - with open('ZenCoders. - `num` in numbers synchronized - datetime d glob. sys.argv[2] . - def myclass `..` @@oscla org. . class { - displ hooks( public static void ma functor: - $myclass->method( impport sys, os.pipe ` @param name` - fcl if(system(cmd) myc. /de ` $card( array("a" srand - format lists: ++: conc ++ "my an WHERE for( == myi - `sys: myvalue(myvalue) sys.t Console.W try{ rais using - connec SELECT * FROM table mycnf acco desc and or selector::clas at - openldap string sys. print "zenc der " { 'a': `ls -l` > appe &firs - import Tkinter paste( $obh &a or it myval bro roll: :: [] require a - case `` super. +y expr say " %rooms 1 --account fb- yy - proc meth Animate => send(D, open) putd EndIf 10 whi myc` cont - and main (--) import loop $$ or end onload UNION WITH tab timer 150 *2 - end. begin True GtkLabel *label doto partition te let auto i<- (i + d ); - .mushup ``/. ^/zenc/ myclass->her flv op <> element >> 71 or - QFileDi : and .. with myc toA channel::bo myc isEmpty a not bodt; - class T public pol str mycalc d pt &&a *i fc add ^ac - ::ZenCoders::core::namespac boost::function st f = std: ;; int assert - cout << endl public genera #include "b ost ::ac myna const cast mys - ac size_t return ran int (*getNextValue)(void) ff double sa_family_t famil - pu a do puts(" ac int main(int argc, char* "%5d struct nam - cs float for typedef enum puts getchar() - if( else #define fp FILE* f char* s - i++ strcat( %s int - 31] total+= do - }do while(1) sle - getc strcpy( a for - prin scanf(%d, & get - int void myfunc(int pa retu - BEQ BNEQZ R1 10 ANDI R1 R2 SYS - XOR SYSCALL 5 SLTIU MFLO 15 SW JAL - BNE BLTZAL R1 1 LUI 001 NOOP MULTU SLLV - MOV R1 ADD R1 R2 JUMP 10 1001 BEQ R1 R2 1 ANDI - 1101 1010001100 111 001 01 1010 101100 1001 100 - 110110 100 0 01 101 01100 100 100 1000100011 - 11101001001 00 11 100 11 10100010 - 000101001001 10 1001 101000101 - 010010010010110101001010 - -For further information: -visit our website www.zencoders.org - -Author: Sebastiano Merlino - -Copying -======= -This manual is for libhttpserver, C++ library for creating an -embedded Rest HTTP server (and more). +# The libhttpserver reference manual + +![GA: Build Status](https://github.com/etr/libhttpserver/actions/workflows/verify-build.yml/badge.svg) +[![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) + +[![Sponsor](https://img.shields.io/badge/Sponsor-%E2%9D%A4-ff69b4?logo=githubsponsors&logoColor=white)](https://github.com/sponsors/etr) + +## Tl;dr + +libhttpserver is a C++20 library for building high-performance RESTful HTTP +servers on top of [GNU libmicrohttpd](https://www.gnu.org/software/libmicrohttpd/). +v2.0 is a lambda-first redesign: a working server is ten lines, handlers are +`std::function`s, responses are value-typed, and every public method is +thread-safe by contract. + +**Features:** + +- HTTP/1.1-compatible request parser +- RESTful-oriented interface +- Lambda-first handler API; class-form handlers (`http_resource`) when state is shared +- Value-typed responses with seven factories and fluent `with_*` chaining +- Cross-platform (Linux, *BSD, macOS, Windows / MSYS2-MinGW) +- Multiple threading models (internal poll, internal select, thread pool, external loop) +- IPv6 and dual-stack support +- Optional TLS (HTTPS) via GnuTLS, including mTLS, SNI, and TLS-PSK +- Optional Basic and Digest authentication (SHA-256, SHA-512/256, MD5) +- Optional centralized authentication with path-based skip rules +- Optional WebSocket support (requires libmicrohttpd built with WebSocket support) +- Response shapes for every common body: string, file, iovec scatter-gather, pipe streaming, deferred callback, empty, and 401-unauthorized +- Incremental POST processing (form-urlencoded, multipart/form-data, file uploads) +- External event-loop integration (`run`, `run_wait`, `get_fdset`, `add_connection`) +- Daemon introspection (bound port, active connections, listen FD) +- Turbo mode, TCP_FASTOPEN, suppressed `Date` headers, configurable backlog +- IP access control with wildcard ranges — deny list (`deny_ip`) and allow list (`allow_ip`), IPv4 & IPv6 +- Architecture contracts you can rely on: threading (DR-008 / §5.1) and error propagation (DR-009 / §5.2) + +This README is a guided reference: it walks the v2.0 API surface section by +section. It is comprehensive but not exhaustive — the headers under +[`src/httpserver/`](src/httpserver/) are the source of truth, and the +[`examples/`](examples/) tree contains a runnable demonstration of every +feature. + +For a visual mental model, see +[`docs/architecture/`](docs/architecture/): a **class, relationship & +filesystem map** and a **request lifecycle & routing flow** diagram +(Mermaid inline on GitHub, plus richer self-contained HTML pages). + +The shortest possible server looks like this: + +```cpp +// Copyright 2026 Sebastiano Merlino +// libhttpserver hello-world example — the lambda form. +// Compiles in ten lines including main(), with no http_resource subclass +// and no raw-pointer ownership. Production code typically qualifies names +// explicitly; the `using namespace` here is a one-off so this file can +// document the shortest possible end-to-end demo. See shared_state.cpp +// for the class-based pattern that is appropriate when handlers must +// share mutable state. +#include +using namespace httpserver; // NOLINT(build/namespaces) - keep the demo at <=10 LOC +int main() { + webserver ws{create_webserver(8080)}; + ws.on_get("/hello", [](const http_request&) { + return http_response::string("Hello, World!"); + }); + ws.start(true); +} +``` + +The block above is reproduced byte-for-byte from +[`examples/hello_world.cpp`](examples/hello_world.cpp); a CI gate +(`scripts/check-readme.sh`) enforces the byte-for-byte equality. + +## Table of contents + +* [Introduction](#introduction) +* [Requirements](#requirements) +* [Building and installation](#building-and-installation) +* [Getting started — Hello, world](#getting-started--hello-world) +* [Class-form handlers](#class-form-handlers) +* [Structures and classes type definition](#structures-and-classes-type-definition) +* [Create and work with a webserver](#create-and-work-with-a-webserver) +* [The resource object](#the-resource-object) +* [Routing](#routing) +* [Request](#request) +* [Response](#response) +* [IP blocking and unblocking](#ip-blocking-and-unblocking) +* [Authentication](#authentication) +* [WebSocket support](#websocket-support) +* [Daemon introspection and external event loops](#daemon-introspection-and-external-event-loops) +* [HTTP utils](#http-utils) +* [Lifecycle hooks](#lifecycle-hooks) +* [Threading contract](#threading-contract) +* [Error propagation](#error-propagation) +* [Feature availability](#feature-availability) +* [Migrating from v1](#migrating-from-v1) +* [Examples](#examples) + +#### Community + +* [Code of Conduct](CODE_OF_CONDUCT.md) +* [Contributing](CONTRIBUTING.md) + +#### Appendices + +* [Copying](#copying) — short statement of the documentation license. +* [GNU-LGPL](COPYING.LESSER) — the GNU Lesser General Public License (the library's code license; full text in `COPYING.LESSER`). +* [GNU-FDL](LICENSE) — the GNU Free Documentation License (this manual's license; full text in `LICENSE`). +* [Thanks](#thanks) + +## Introduction + +libhttpserver is meant to constitute an easy system to build HTTP servers in +the REST fashion. It is built on top of [libmicrohttpd](https://www.gnu.org/software/libmicrohttpd/) +and, like its substrate, it is a daemon library. The mission is to expose +every useful HTTP capability through a simple, modern C++ API so that +application code can focus on business logic rather than on the mechanics of +HTTP request handling. + +**Design.** libhttpserver is lambda-first. The shortest server is ten +lines and uses no inheritance; handlers are +`std::function`; responses are +value-typed with seven factories and fluent `with_*` chaining; ownership +of resource-form handlers is expressed in `std::unique_ptr` and +`std::shared_ptr`. The class form (`http_resource`) is the right shape +when several methods on one path share mutable state. + +**Two contracts are load-bearing** — and both are stated explicitly in this +README rather than left implicit: + +- **Thread-safety** (DR-008 / §5.1). Every public method on `webserver` is + thread-safe and re-entrant from inside a handler, with two + documented exceptions (`stop()`, `~webserver()`) that deadlock if invoked + from a handler thread. +- **Error propagation** (DR-009 / §5.2). A handler that throws is caught + by the dispatcher and routed through `internal_error_handler`. There is + no throw-as-status idiom — to send a 404 from a handler, build it by value + with `http_response::empty().with_status(404)`. + +libhttpserver decodes form bodies automatically: `application/x-www-form-urlencoded` +and `multipart/form-data` are parsed into the request's argument map. Files +attached to multipart uploads are exposed through `http_request::get_files()`. + +All functions are reentrant and thread-safe unless explicitly stated otherwise. +Clients can also specify resource limits on overall connection count, per-IP +connection count, and per-connection memory to avoid resource exhaustion. + +[Back to TOC](#table-of-contents) + +## Requirements + +libhttpserver can be used without any dependencies aside from libmicrohttpd. + +The minimum versions required are: + +* g++ >= 11 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` you already have this) + +For versions before 0.18.0, on MinGW you will need: + +* libgnurx >= 2.5.1 + +The test cases use [libcurl](http://curl.haxx.se/libcurl/) but you don't need +it to compile the library itself. + +Please refer to the readme file for your particular distribution if there is +one for important notes. + +| Platform | Toolchain | Notes | +|---|---|---| +| Debian 13 (trixie) | GCC 14.2 | Out-of-the-box | +| RHEL 9 | `gcc-toolset-14` | Stock GCC 11 is too old; install the Red Hat toolset overlay | +| RHEL 10 | GCC 14 | Out-of-the-box | +| FreeBSD 14.x | base Clang 18+ | Out-of-the-box | +| macOS | Homebrew GCC 15+ or current Apple Clang | Out-of-the-box | +| vcpkg / Conan | GCC 13+ / Clang 16+ | Out-of-the-box | + +[Back to TOC](#table-of-contents) + +## Building and installation + +libhttpserver uses the standard autotools workflow. The usual build process is: + +```sh +./bootstrap +mkdir build +cd build +../configure +make +sudo make install +``` + +`make check` runs the test suite (unit + integration). `make examples` +builds every program under [`examples/`](examples/). `make dist` produces +a portable source tarball. + +**ABI / packaging.** SOVERSION is 2. Distributions package +`libhttpserver2`, parallel-installable with prior major versions so +applications can link the major they need side by side. There is no +inline namespace and no symbol-versioning script. + +[Back to TOC](#table-of-contents) + +### Optional parameters to the configure script + +A complete list of parameters can be obtained by running `./configure --help`. +The libhttpserver-specific options are listed below (the canonical configure +options are also supported): + +* `--enable-same-directory-build` — enable in-tree builds. Strongly discouraged. *(default: no)* +* `--enable-debug` — enable debug data generation. *(default: no)* +* `--disable-doxygen-doc` — don't generate any Doxygen documentation. Doxygen is automatically invoked when present on the system and disabled otherwise. +* `--enable-fastopen` — enable use of TCP_FASTOPEN. *(default: yes)* +* `--enable-static` — enable static linking. *(default: yes)* + +Build-time feature flags (auto-detected, can be forced): + +* `--with-gnutls` / `--without-gnutls` — enable/disable TLS support. Sets the `HAVE_GNUTLS` macro. +* `--with-basic-auth` / `--without-basic-auth` — enable/disable Basic auth. Sets `HAVE_BAUTH`. +* `--with-digest-auth` / `--without-digest-auth` — enable/disable Digest auth. Sets `HAVE_DAUTH`. +* `--with-websocket` / `--without-websocket` — enable/disable WebSocket support. Sets `HAVE_WEBSOCKET`. Requires libmicrohttpd built with WebSocket support. + +See [Feature availability](#feature-availability) for how each of these flags +affects the runtime API. + +[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: + +```sh +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/) (>= 1.0.0). +5. Build libhttpserver: + +```sh +./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 there. + +#### 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 emit a warning when +building in this environment: + +``` +configure: WARNING: Building from MSYS environment. Binaries will depend on msys-2.0.dll. +``` + +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): + +```sh +g++ myapp.cpp $(pkg-config --cflags --libs libhttpserver) -o myapp +``` + +Manual linking: + +```sh +g++ myapp.cpp -I/mingw64/include -L/mingw64/lib -lhttpserver -lmicrohttpd -o myapp +``` + +[Back to TOC](#table-of-contents) + +## Getting started — Hello, world + +The Tl;dr at the top of this README contains the entire program. Walking +through it: + +* `webserver ws{create_webserver(8080)};` — `create_webserver` is a fluent + builder for the server configuration; `webserver` is constructed by + direct initialisation from the builder. `webserver` is non-copyable and + non-movable; pass it by reference once it exists. The constructor is + `explicit`, so callers must direct-init rather than rely on implicit + conversion. +* `ws.on_get("/hello", [](const http_request&) { ... });` — register a + GET-only handler. The handler is a + `std::function`. There is no + subclass, no `shared_ptr`, no raw pointer. The seven HTTP-verb + shortcuts are `on_get`, `on_post`, `on_put`, `on_delete`, `on_patch`, + `on_options`, and `on_head`. For CONNECT and TRACE, use the + `route(http_method::CONNECT, ...)` and `route(http_method::TRACE, ...)` + forms covered under [Routing](#routing). +* `return http_response::string("Hello, World!");` — `http_response` is + a value type. Factories on `http_response` build common shapes; the + fluent `with_*` mutators add headers, footers, cookies, and status. + The response is returned by value into the dispatcher. +* `ws.start(true);` — `true` means *block this thread until the server + is stopped*. Pass `false` to start the listener and return immediately; + later call `stop_and_wait()` from another thread. + +To test the example, you can run the following from a terminal: + +```sh +curl -XGET -v http://localhost:8080/hello +``` + +See [`examples/hello_world.cpp`](examples/hello_world.cpp) and +[`examples/hello_with_get_arg.cpp`](examples/hello_with_get_arg.cpp) for +the complete sources. + +[Back to TOC](#table-of-contents) + +## Class-form handlers + +Lambdas suffice when each HTTP method is independent. When several methods +on one path share state — a counter, a cache, a mutex — derive from +[`http_resource`](src/httpserver/http_resource.hpp) and register the +subclass once: + +```cpp +class counter : public httpserver::http_resource { + public: + httpserver::http_response render_get(const httpserver::http_request&) override { + std::lock_guard lock{m_}; + return httpserver::http_response::string(std::to_string(n_)); + } + httpserver::http_response render_post(const httpserver::http_request&) override { + std::lock_guard lock{m_}; + ++n_; + return httpserver::http_response::string(std::to_string(n_)); + } + private: + std::mutex m_; + int n_ = 0; +}; + +// ... +ws.register_path("/count", std::make_unique()); +``` + +The virtual hooks are `render_get`, `render_post`, `render_put`, +`render_delete`, `render_head`, `render_options`, `render_patch`, +`render_connect`, and `render_trace` — all lowercase, all returning +`http_response` by value. The `webserver` takes ownership of the +resource via `std::unique_ptr` (or a `std::shared_ptr` overload — see +[Routing](#routing)). + +See [`examples/shared_state.cpp`](examples/shared_state.cpp) for the +canonical example. + +[Back to TOC](#table-of-contents) + +## Structures and classes type definition + +This is the cast of types you will work with. Each is detailed further on +in its own section. + +* **`webserver`** — represents the daemon listening on a socket for HTTP + traffic. Non-copyable, non-movable. See [Create and work with a + webserver](#create-and-work-with-a-webserver). + * **`create_webserver`** — fluent builder for the configuration that + `webserver`'s `explicit` constructor consumes. See the same section. +* **`http_resource`** — base class for a logical collection of HTTP + methods bound to a path. Override the snake_case `render_*` virtuals. + See [The resource object](#the-resource-object). +* **`http_request`** — the request passed to handlers. Read-only, + single-threaded per request. See [Request](#request). +* **`http_response`** — the value-typed response. Seven factories cover + every body shape; fluent `with_*` mutators add headers, footers, + cookies, and status. See [Response](#response). + * Factories: `http_response::string`, `http_response::file`, + `http_response::iovec`, `http_response::pipe`, `http_response::empty`, + `http_response::deferred`, `http_response::unauthorized`. +* **`http_method`** — strongly-typed enum of HTTP verbs (GET, POST, PUT, + DELETE, HEAD, OPTIONS, PATCH, CONNECT, TRACE). Declared in + [`http_method.hpp`](src/httpserver/http_method.hpp). +* **`method_set`** — small bitset of `http_method` values, used by + `route()` for atomic multi-method registration. +* **`iovec_entry`** — element type for `http_response::iovec` scatter-gather + responses. A `{ void* base, size_t length }` pair where the base + pointer is borrowed for the lifetime of the response. +* **`websocket_handler`** — base class for handling WebSocket + connections; derive and implement `on_message`. See [WebSocket + support](#websocket-support). + * **`websocket_session`** — represents an active WebSocket connection; + methods to send text, binary, ping/pong, and close frames. +* **`feature_unavailable`** — `std::runtime_error` subclass thrown when + application code requests a feature that was compiled out (no TLS, + no Basic auth, no Digest auth, no WebSocket). See [Feature + availability](#feature-availability). + +[Back to TOC](#table-of-contents) + +## Create and work with a webserver + +Creating a webserver with a standard configuration is the one-liner shown +in the Tl;dr: + +```cpp +webserver ws{create_webserver(8080)}; +``` + +`create_webserver` is a *fluent builder*: every setter returns `*this` so +calls chain. The `webserver` constructor is **`explicit`**: callers must +direct-initialise (`webserver ws{cw};`) rather than rely on implicit +conversion. The builder validates eagerly where the input domain +is well-defined — `port` rejects values outside `[0, 65535]`, every `int` +setter rejects negatives, etc. — and raises `std::invalid_argument` at the +setter. Feature-gated settings (`use_ssl`, `basic_auth`, `digest_auth`, +WebSocket registration) are validated by the `webserver` constructor, not +the setter; so a builder configured for an unsupported feature throws +`feature_unavailable` from `webserver(create_webserver)` rather than from +the chained call. + +A complete chained example: + +```cpp +webserver ws{create_webserver(8080) + .max_threads(4) + .max_connections(1024) + .connection_timeout(30) + .start_method(http::http_utils::INTERNAL_SELECT) + .turbo() + .suppress_date_header()}; +``` + +The rest of this section is a reference of every group of options. + +### Basic startup options + +* **`.port(uint16_t p)` / `.port(int p)`** — the port the server listens + on. Can also be passed to the `create_webserver` constructor: + `create_webserver(8080)`. The `int` overload exists so that + out-of-`uint16_t` values like `70000` are expressible and rejected with + `std::invalid_argument`; the `uint16_t` overload keeps type-safe call + sites clean. +* **`.max_connections(int n)`** — maximum number of concurrent connections + to accept. The default is `FD_SETSIZE − 4` (the maximum number of file + descriptors supported by `select` minus four for `stdin`, `stdout`, + `stderr`, and the server socket). That is, the default is as large as + possible. Note that if you set a low limit, browsers doing request + pipelining can saturate it. +* **`.max_threads(int n)`** — size of the internal worker pool, or 0 to + spawn a thread per connection. Default 0. +* **`.memory_limit(int bytes)`** — per-connection memory limit. Default 0 + (libmicrohttpd default ≈ 32 kB). +* **`.content_size_limit(size_t bytes)`** — cap on individual request + body size. Default `SIZE_MAX` (unlimited). +* **`.connection_timeout(int seconds)`** — idle timeout for a + connection. Default 180. +* **`.per_IP_connection_limit(int n)`** — cap on concurrent connections + from any single client IP. Default 0 (no limit). +* **`.max_thread_stack_size(int bytes)`** — stack size for internal + worker threads. Default 0 (libmicrohttpd default). + +### Listener, socket, and threading options + +* **`.start_method(http::http_utils::start_method_T m)`** — pick the + libmicrohttpd polling mode. Values include `INTERNAL_SELECT` (default), + `INTERNAL_POLL`, `INTERNAL_EPOLL`, and `EXTERNAL_SELECT` (for use with + `run` / `run_wait`). +* **`.bind_address(const sockaddr* a)` / `.bind_address(const std::string& ip)`** — bind + to a specific address. The string overload accepts either IPv4 + (`"127.0.0.1"`) or IPv6 (`"::1"`). +* **`.bind_socket(int fd)`** — use a pre-existing listening socket. + Useful for socket-activation setups (systemd, launchd). +* **`.use_ipv6(bool = true)`** — bind to an IPv6 socket. (Positive-polarity + form; see [`RELEASE_NOTES.md`](RELEASE_NOTES.md) for the rename.) +* **`.use_dual_stack(bool = true)`** — accept both IPv4 and IPv6 on the + same socket. +* **`.listen_socket(bool = true)`** — own the listening socket. Pass + `false` when feeding connections in via `add_connection()`. +* **`.thread_safety(bool = true)`** — internal locking. Defaults to + `true`; disable only if you understand the consequences. +* **`.alpn(bool = true)`** — enable ALPN negotiation for HTTPS. +* **`.listen_backlog(int n)`** — `listen(2)` backlog. Default 0 + (libmicrohttpd default). +* **`.address_reuse(int v)`** — `SO_REUSEADDR` / `SO_REUSEPORT` mode. +* **`.tcp_nodelay(bool = true)`** — disable Nagle on accepted sockets. +* **`.tcp_fastopen_queue_size(int n)`** — TCP_FASTOPEN queue size. + Requires `--enable-fastopen` at configure time. +* **`.connection_memory_increment(size_t v)`** — incremental memory + allocation per connection. +* **`.sigpipe_handled_by_app(bool = true)`** — tell libmicrohttpd that + the application installs its own `SIGPIPE` handler. +* **`.client_discipline_level(int v)`** — strictness level for malformed + request handling. `-1` means default. +* **`.turbo(bool = true)`** — enable libmicrohttpd's `USE_TURBO` flag + (sacrifices some safety checks for performance). +* **`.suppress_date_header(bool = true)`** — omit the `Date:` response + header (small perf win at the cost of one piece of HTTP metadata). + +### Logging and validation + +* **`.log_access(std::function cb)`** — invoked + once per accepted request with a single-line summary suitable for an + access log. Runs from MHD worker threads; **must be thread-safe**. +* **`.log_error(std::function cb)`** — invoked + for errors the library decides are worth reporting. Same thread-safety + contract. +* **`.validator(std::function v)`** — called + early on every request; returning `false` rejects the request. +* **`.unescaper(std::function<...> u)`** — overrides the URL-unescape + step. Default unescaper handles the canonical HTTP escapes. + +### Feature toggles (positive polarity) + +* **`.basic_auth(bool = true)`** — enable Basic auth handling. +* **`.digest_auth(bool = true)`** — enable Digest auth handling. +* **`.use_ssl(bool = true)`** — enable TLS. +* **`.deferred(bool = true)`** — enable libmicrohttpd's deferred-response + internal optimisations. Required to use `http_response::deferred(...)`. +* **`.debug(bool = true)`** — verbose libmicrohttpd debug output. +* **`.pedantic(bool = true)`** — strict HTTP-RFC parsing. +* **`.regex_checking(bool = true)`** — validate path regexes at + registration. Default `true`. +* **`.ip_access_control(bool = true)`** — enable the IP access-control + machinery used by `deny_ip` / `allow_ip` (and their `remove_*` + counterparts). Default `true`. When `false`, every connection is + admitted and both lists are ignored. +* **`.post_process(bool = true)`** — auto-parse `application/x-www-form-urlencoded` + and `multipart/form-data` bodies into the request's argument map. + Default `true`. +* **`.put_processed_data_to_content(bool = true)`** — also expose + processed POST data through the raw content accessor. +* **`.single_resource(bool = true)`** — short-circuit the resource lookup + when only one resource is registered. + +> The feature toggles all use positive polarity: pass `false` to disable +> (`basic_auth(false)`, `use_ssl(false)`, etc.). + +### TLS (HTTPS) options + +These are no-ops unless the build has GnuTLS support. On a GnuTLS-off +build, `webserver(create_webserver{}.use_ssl(true))` throws +`feature_unavailable`. + +* **`.https_mem_key(const std::string& path)`** — load the PEM key from + the file at `path`. +* **`.https_mem_cert(const std::string& path)`** — load the PEM + certificate from the file at `path`. +* **`.https_mem_trust(const std::string& path)`** — load CA certificate(s) + for verifying client certificates (mTLS). +* **`.raw_https_mem_key(const std::string& pem)`** + / **`.raw_https_mem_cert(...)`** / **`.raw_https_mem_trust(...)`** — + pass PEM material directly without reading a file. +* **`.https_mem_dhparams(const std::string& pem)`** — explicit DH + parameters. +* **`.https_key_password(const std::string& pw)`** — passphrase for an + encrypted private key. +* **`.https_priorities(const std::string& s)`** — GnuTLS priority + string. +* **`.https_priorities_append(const std::string& s)`** — append to the + default priority string. +* **`.cred_type(http::http_utils::cred_type_T t)`** — `CERTIFICATE` / + `PSK` / `NONE`. Default `NONE`. +* **`.psk_cred_handler(...)`** — PSK lookup callback for TLS-PSK. +* **`.sni_callback(...)`** — Server Name Indication lookup callback + (returns the PEM `(cert, key)` pair for a hostname). + +### Authentication setup + +* **`.digest_auth_random(const std::string& s)`** — server-side random + seed for digest nonces. +* **`.nonce_nc_size(int n)`** — replay-protection window size for + digest auth. +* **`.auth_handler(auth_handler_ptr cb)`** — install a centralized + authentication handler that runs before every resource's `render_*` + call. The callback returns `std::nullopt` to allow the request to + proceed, or an `http_response` (typically + `http_response::unauthorized(realm)`) to reject it. Single source of + truth for auth — see [Centralized + authentication](#using-centralized-authentication). +* **`.auth_skip_paths(const std::vector& paths)`** — paths + to bypass `auth_handler`. Exact match (`"/health"`) and wildcard + suffixes (`"/public/*"`) are both supported. + +### Custom error handlers + +* **`.not_found_handler(error_handler h)`** — invoked when no resource + matches the request path (HTTP 404). The handler returns + `http_response` by value. +* **`.method_not_allowed_handler(error_handler h)`** — invoked when a + resource matches the path but not the HTTP method (HTTP 405). +* **`.internal_error_handler(internal_error_handler_t h)`** — invoked + when a handler throws or the library hits an internal failure during + dispatch. The handler signature is + `http_response(const http_request&, std::string_view message)`. Load-bearing: + see [Error propagation](#error-propagation). +* **`.expose_exception_messages(bool = true)`** — restores the v1 + behaviour of including the originating exception message in the + **default** 500 response body. Default is `false`: the default body + is the fixed string `"Internal Server Error"` + ([DR-009 Revision 1](specs/architecture/11-decisions/DR-009.md), + CWE-209 information-disclosure fix). The configured `log_error` + callback continues to receive the verbatim message regardless of + this flag — only the HTTP response body is affected. + **Warning:** exception messages routinely contain file paths, SQL + fragments, internal identifiers, and attacker-influenced input. + Enable only in development or behind an explicit `#ifndef NDEBUG` + guard. A configured `internal_error_handler` is unaffected — it + always receives the message and can build any body it wants. + +A worked example: + +```cpp +auto cfg = httpserver::create_webserver(8080) + .not_found_handler([](const httpserver::http_request&) { + return httpserver::http_response::string("nope").with_status(404); + }) + .method_not_allowed_handler([](const httpserver::http_request&) { + return httpserver::http_response::empty().with_status(405); + }) + .internal_error_handler([](const httpserver::http_request&, std::string_view what) { + // CWE-209: 'what' may contain file paths, SQL fragments, or + // attacker-influenced input. Log it internally; do NOT echo + // it to the HTTP client. + (void)what; // e.g. logger->error(what); + return httpserver::http_response::string("Internal Server Error").with_status(500); + }); +httpserver::webserver ws{cfg}; +``` + +### File-upload options + +* **`.file_upload_target(file_upload_target_T t)`** — + `FILE_UPLOAD_MEMORY_ONLY` (default) keeps uploads in memory; + `FILE_UPLOAD_DISK_ONLY` writes to disk; `FILE_UPLOAD_MEMORY_AND_DISK` + does both. +* **`.file_upload_dir(const std::string& dir)`** — directory for + on-disk uploads. Must not be empty. +* **`.generate_random_filename_on_upload(bool = true)`** — name uploaded + files randomly (vs. trust the client's `Content-Disposition: filename=`). +* **`.file_cleanup_callback(file_cleanup_callback_ptr cb)`** — invoked + after request completion to clean up uploaded files. Return `true` to + delete the file from disk, `false` to keep it. + +### Daemon / external event-loop options + +These dovetail with the runtime methods covered under [Daemon +introspection and external event loops](#daemon-introspection-and-external-event-loops). +The `default_policy` selects which list is the exception list — i.e. what +happens to an address on neither list: + +* **`.default_policy(http::http_utils::policy_T p)`** — `ACCEPT` (default) + admits every address except those on the deny list (`deny_ip`); `REJECT` + refuses every address except those on the allow list (`allow_ip`). Under + either policy an allow entry overrides a matching deny entry. + +### Runtime operations on `webserver` + +Once constructed, the `webserver` instance exposes: + +* **`bool start(bool blocking = false)`** — start listening. When + `blocking == true`, this thread blocks until the server stops. +* **`bool stop()`** — stop the daemon and join libmicrohttpd's worker + threads. **Calling this from inside a handler deadlocks** — see + [Threading contract](#threading-contract). +* **`void stop_and_wait()`** — `stop()` then wait until every in-flight + handler has returned. Same deadlock contract as `stop()`. +* **`bool is_running()`** — true if the daemon is currently accepting + connections. + +Registration verbs (covered in detail under [Routing](#routing)): +`on_get`, `on_post`, `on_put`, `on_delete`, `on_patch`, `on_options`, +`on_head`, `route(...)`, `register_path(...)`, `register_prefix(...)`, +`register_ws_resource(...)`, and the matching `unregister_*` forms. + +[Back to TOC](#table-of-contents) + +## The resource object + +The `http_resource` class represents a logical collection of HTTP methods +that will be associated with a URL when registered on the webserver. The +class is **designed for extension**. When the webserver matches a request +against a resource, the method corresponding to the request's HTTP verb +is called on the resource. + +The `http_resource` class contains the following extensible methods +(also called *handlers* or *render methods*) — every one returns +`http_response` by value: + +* **`http_response render_get(const http_request& req)`** — invoked on an HTTP GET request. +* **`http_response render_post(const http_request& req)`** — invoked on an HTTP POST request. +* **`http_response render_put(const http_request& req)`** — invoked on an HTTP PUT request. +* **`http_response render_head(const http_request& req)`** — invoked on an HTTP HEAD request. +* **`http_response render_delete(const http_request& req)`** — invoked on an HTTP DELETE request. +* **`http_response render_trace(const http_request& req)`** — invoked on an HTTP TRACE request. +* **`http_response render_options(const http_request& req)`** — invoked on an HTTP OPTIONS request. +* **`http_response render_connect(const http_request& req)`** — invoked on an HTTP CONNECT request. +* **`http_response render_patch(const http_request& req)`** — invoked on an HTTP PATCH request. +* **`http_response render(const http_request& req)`** — fallback invoked when no method-specific override is provided. + +These methods are all `virtual`; override only the verbs your resource +supports. Unhandled verbs fall through to `render()`; if the resource also +does not override `render()`, the dispatch returns the configured 405 +response. + +### Restricting allowed methods + +By default, every HTTP verb falls through to `render()`. To narrow a +resource to a specific subset of methods, use the `set_allowing` / +`disallow_all` API: + +```cpp +class read_only : public httpserver::http_resource { + public: + read_only() { + disallow_all(); + set_allowing("GET", true); + set_allowing("HEAD", true); + } + httpserver::http_response render_get(const httpserver::http_request&) override { + return httpserver::http_response::string("ok"); + } +}; +``` + +Requests for disallowed verbs are short-circuited by the dispatcher and +land in the `method_not_allowed_handler` (HTTP 405) without calling +`render_*`. See [`examples/allowing_disallowing_methods.cpp`](examples/allowing_disallowing_methods.cpp) +for a worked example. + +### Ownership + +Resources are passed to the webserver via `std::unique_ptr` (the +webserver takes exclusive ownership) or `std::shared_ptr` (caller and +webserver share ownership). Ownership is always expressed through a +smart pointer. + +[Back to TOC](#table-of-contents) + +## Routing + +The `webserver` exposes three families of registration entry points. +They are all interoperable: within one server, some paths can be +lambdas and others can be `http_resource` subclasses. + +### Lambda form: per-method, exact path + +`on_get`, `on_post`, `on_put`, `on_delete`, `on_patch`, `on_options`, +`on_head` — each takes an exact path and a +`std::function`: + +```cpp +ws.on_get("/hello", [](const http_request&) { + return http_response::string("Hello, World!"); +}); +ws.on_post("/items", [](const http_request& req) { + return http_response::string("created: " + std::string{req.get_arg("name")}) + .with_status(201); +}); +``` + +Re-registering the same `(method, path)` pair throws. The seven +shortcuts cover the seven verbs with first-class handler functions; +CONNECT and TRACE go through `route()` (below). + +### Lambda form: atomic multi-method via `route()` + +`route()` is the primary escape hatch when the HTTP method is known +only at runtime (e.g. loaded from config, selected from a dispatch +table). The single-method form: + +```cpp +route(http_method m, "/info", handler) +``` + +takes a runtime `http_method` value and is the canonical form for +config-driven or table-driven registration (PRD-HDL-REQ-006). + +`route(method_set methods, "/info", handler)` additionally allows +registering a handler under several methods in a single critical +section — either every slot is registered, or none of them are: + +```cpp +ws.route(http_method::GET | http_method::HEAD, "/info", + [](const http_request&) { + return http_response::string("info"); + }); +``` + +A `method_set` is the bitwise-or of `http_method` values. This is the +only entry point through which CONNECT and TRACE are reachable as +lambdas: `route(http_method::CONNECT, "/proxy", handler)`. + +### Resource form: `register_path` and `register_prefix` + +For `http_resource` subclasses (the [class form](#class-form-handlers)): + +```cpp +ws.register_path("/count", std::make_unique()); // exact match +ws.register_prefix("/static", std::make_unique()); // subtree match +``` + +Both methods have `std::shared_ptr` overloads for the +case where you need to retain a reference to the resource: + +```cpp +auto cnt = std::make_shared(); +ws.register_path("/count", cnt); +// cnt is still usable from outside the webserver. +``` + +**Parameterised paths.** Brace syntax captures path segments: + +```cpp +ws.register_path("/users/{id}", std::make_unique()); +``` + +The captured value is available inside the handler via +`http_request::get_arg("id")`. + +**Per-segment regex constraints.** Add a regex after a pipe to constrain +a segment: + +```cpp +ws.register_path("/users/{id|[0-9]+}", std::make_unique()); +``` + +Only requests where the `id` segment matches `[0-9]+` will match this +registration. + +Regex validation is on by default; disable with `regex_checking(false)` +on the builder if you need pure literal-path semantics. + +**Unregistration.** `unregister_path(path)` and `unregister_prefix(path)` +remove an exact-match or prefix-match registration respectively. +`unregister_resource(path)` is a kind-agnostic convenience that atomically +clears whichever kind (exact or prefix) is registered at `path` — use it +when the caller does not track how the resource was registered. + +## Request + +`http_request` is read-only inside a handler. The accessors are designed +around `std::string_view` so reading headers and arguments does not +allocate. + +### Path, method, version + +| Accessor | Returns | Notes | +|---|---|---| +| `get_path()` | `std::string_view` | The decoded path | +| `get_path_pieces()` | `const std::vector<...>&` | Split path components | +| `get_method()` | `httpserver::http_method` | Strongly-typed enum; see [`http_method.hpp`](src/httpserver/http_method.hpp) | +| `get_version()` | `std::string_view` | `"HTTP/1.1"`, `"HTTP/2"`, … | +| `get_querystring()` | `std::string_view` | Raw query string (no decoding) | +| `get_connection_id()` | uintptr-style id | Stable identifier for the underlying connection | +| `get_requestor()` | `std::string_view` | Connecting peer's IP address (text form) | +| `get_requestor_port()` | `unsigned short` | Connecting peer's port | + +### Headers, arguments, cookies + +| Accessor | Returns | Notes | +|---|---|---| +| `get_headers()` | `const map&` | All request headers | +| `get_header(name)` | `std::string_view` | First value for a named header | +| `get_args()` | `const map&` | All query / form arguments | +| `get_arg(name)` | `std::string_view` | First value for a query / form arg | +| `get_arg_flat(name)` | `std::string_view` | Alias for `get_arg`; explicit "first value only" form | +| `get_cookies()` | `const map&` | All cookies | +| `get_cookie(name)` | `std::string_view` | First value for a named cookie | +| `get_footers()` | `const map&` | Chunked trailers | + +### Uploaded files (multipart) + +| Accessor | Returns | Notes | +|---|---|---| +| `get_files()` | `const map&` | Uploaded files keyed by form field name | +| `get_content()` | `std::string_view` | The raw request body | +| `get_content_size_limit()` | `size_t` | Configured cap, in bytes | + +See [`examples/file_upload.cpp`](examples/file_upload.cpp) and +[`examples/file_upload_with_callback.cpp`](examples/file_upload_with_callback.cpp) +for working programs. + +### Authentication accessors + +| Accessor | Returns | Notes | +|---|---|---| +| `get_user()` | `std::string_view` | Basic-auth user; empty when `HAVE_BAUTH` is off | +| `get_pass()` | `std::string_view` | Basic-auth password; empty when `HAVE_BAUTH` is off | +| `get_digested_user()` | `std::string_view` | Digest-auth user; empty when `HAVE_DAUTH` is off | +| `check_digest_auth(realm, password, nonce_timeout, signal_stale, algo)` | `digest_auth_result` | Validates a digest auth response against a plaintext password | +| `check_digest_auth_digest(realm, ha1, ...)` | `digest_auth_result` | Same as above but against a pre-computed HA1 hash | + +> **Security note.** Basic auth (`get_user`/`get_pass`) transmits +> credentials as Base64 — effectively cleartext. Digest auth +> (`get_digested_user`) avoids transmitting the password but is still +> vulnerable to man-in-the-middle attacks without TLS. Both are only +> safe when the server is configured with TLS (`HAVE_GNUTLS`, `.use_ssl(true)`). +> See [Feature availability](#feature-availability) and +> [`examples/basic_authentication.cpp`](examples/basic_authentication.cpp). + +`digest_auth_result` is a strongly-typed enum: + +* `OK` — authentication succeeded. +* `NONCE_STALE` — the nonce is stale; signal the client to retry with a fresh nonce by setting `signal_stale = true` in the response. +* `WRONG_USERNAME`, `WRONG_REALM`, `WRONG_URI`, `WRONG_QOP`, `WRONG_ALGO`, `RESPONSE_WRONG` — specific reasons for failure. +* `WRONG_HEADER`, `TOO_LARGE`, `NONCE_WRONG`, `NONCE_OTHER_COND`, `ERROR` — other failure conditions. + +### TLS / client-certificate accessors + +When the build was compiled with GnuTLS (`HAVE_GNUTLS`) **and** the +client presented an X.509 certificate during the TLS handshake, these +accessors return the certificate details. On a non-TLS build, or when +the client did not present a certificate, they return empty / `-1` / +`false`: + +| Accessor | Returns | Notes | +|---|---|---| +| `has_client_certificate()` | `bool` | True if a client cert was presented | +| `is_client_cert_verified()` | `bool` | True if the chain validated against the configured trust store | +| `get_client_cert_dn()` | `std::string_view` | Subject Distinguished Name | +| `get_client_cert_issuer_dn()` | `std::string_view` | Issuer Distinguished Name | +| `get_client_cert_cn()` | `std::string_view` | Subject Common Name | +| `get_client_cert_fingerprint_sha256()` | `std::string_view` | Hex-encoded SHA-256 fingerprint | +| `get_client_cert_not_before()` | `time_t` | Validity-start timestamp; `-1` if unavailable | +| `get_client_cert_not_after()` | `time_t` | Validity-end timestamp; `-1` if unavailable | + +See [`examples/client_cert_auth.cpp`](examples/client_cert_auth.cpp) for a +worked mTLS example. + +### Lifetime contract + +Every `string_view` returned by `http_request` is valid for the duration +of the handler invocation **and no longer**. Copy what you need to keep +(e.g. into a `std::string`); do not hand a view to a deferred callback. +The references returned by `get_headers()`, `get_args()`, +`get_path_pieces()`, `get_files()`, and `get_cookies()` follow the same +rule. `http_request` is **single-threaded per request**: sharing one +`http_request` across threads is undefined. + +### Method enum + +`http_method` (declared in +[`http_method.hpp`](src/httpserver/http_method.hpp)) covers the canonical +HTTP methods. `method_set` is a bitset used by atomic multi-method +registration on `route()` (see [Routing](#routing)). + +[Back to TOC](#table-of-contents) + +## Response + +`http_response` is a **value type** — move-only, returned by value, +never `shared_ptr`-wrapped. There is no class hierarchy of body +subclasses; the body shape is a runtime detail of one type. Build a +response with one of the seven factories described below and decorate +it with the fluent `with_*` mutators. + +### The seven factories + +| Factory | Body shape | Use when | +|---|---|---| +| `http_response::string(body, [status, content_type])` | In-memory string (small bodies live inline via SBO) | The body is already in memory | +| `http_response::file(path)` | Stream a file from disk | The body is a static or generated file on disk | +| `http_response::iovec(entries)` | Scatter-gather over a vector of `iovec_entry` (zero-copy) | The body is assembled from several existing buffers | +| `http_response::pipe(fd)` | Stream from a pipe / FIFO | The body is being produced by another process or thread | +| `http_response::empty([status])` | Empty body | 204 No Content, redirects, HEAD responses | +| `http_response::deferred(producer, [closure, content_type])` | Body produced incrementally by a callback | The body cannot be materialised up-front (long-poll, streaming) | +| `http_response::unauthorized(realm, [status, content_type, algorithm])` | 401 with the proper `WWW-Authenticate` header | Reject a request that lacks valid credentials | + +`iovec_entry` is the element type of the `iovec()` vector: + +```cpp +struct iovec_entry { + const void* base; // borrowed for the response's lifetime + size_t length; +}; +``` + +The `base` pointer is **borrowed**: the caller must keep the underlying +storage alive until the response has been fully written to the wire. +For owning scatter-gather, copy your strings into a `std::vector` +that the response captures by value. + +### Fluent mutation + +Every `http_response` exposes `with_status`, `with_header`, `with_footer`, +and `with_cookie`. These return `*this` (by reference on lvalues, by +rvalue-reference on rvalues), so calls can chain: + +```cpp +return httpserver::http_response::string("hi") + .with_header("X-Trace-Id", trace_id) + .with_status(201); +``` + +The fluent helpers are intentionally narrow — every other mutation +(content type, body data) is set at the factory call. This keeps the +"build it, return it, done" idiom uniform across response shapes. + +### Building error responses by value + +There is **no throw-as-status idiom**. To return a 404 from a handler, +build it explicitly: + +```cpp +if (!found) { + return httpserver::http_response::empty().with_status(404); +} +``` + +Or with a body: + +```cpp +return httpserver::http_response::string("user not found").with_status(404); +``` + +For 401 responses, the dedicated `http_response::unauthorized` factory +sets the right `WWW-Authenticate` header for you: + +```cpp +return httpserver::http_response::unauthorized("MyRealm"); +``` + +See [`examples/setting_headers.cpp`](examples/setting_headers.cpp), +[`examples/iovec_response_example.cpp`](examples/iovec_response_example.cpp), +[`examples/minimal_file_response.cpp`](examples/minimal_file_response.cpp), +[`examples/pipe_response_example.cpp`](examples/pipe_response_example.cpp), +[`examples/empty_response_example.cpp`](examples/empty_response_example.cpp), +[`examples/minimal_deferred.cpp`](examples/minimal_deferred.cpp), and +[`examples/binary_buffer_response.cpp`](examples/binary_buffer_response.cpp) +for working programs covering every response shape. + +[Back to TOC](#table-of-contents) + +## IP blocking and unblocking + +libhttpserver supports both IPv4 and IPv6 and manages them transparently. +The only requirement for IPv6 is that it is enabled on the underlying +server — set `use_ipv6(true)` on `create_webserver` (or `use_dual_stack(true)` +for both stacks on the same socket). + +You can populate the deny list and the allow list (with individual IPs or +wildcard ranges) at runtime using these methods on `webserver`: + +* **`void deny_ip(std::string_view ip)`** — add one IP (or a range, + e.g. `"127.0.0.*"`) to the deny list. Connections from a matching + address are refused at the policy callback. This is the exception list + under the default `ACCEPT` policy. +* **`void remove_denied_ip(std::string_view ip)`** — remove one IP (or a + range) from the deny list. Idempotent: removing an entry that is not + currently denied is a no-op. +* **`void allow_ip(std::string_view ip)`** — add one IP (or a range) to + the allow list. This is the exception list under the `REJECT` policy + (permit only these). Under `ACCEPT`, an allow entry overrides a + matching `deny_ip` entry. +* **`void remove_allowed_ip(std::string_view ip)`** — remove one IP (or a + range) from the allow list. Idempotent. + +### IP string format + +The IP string format can represent both IPv4 and IPv6. Addresses are +normalised internally by the webserver to a common representation, so +any valid IPv4 or IPv6 textual representation works. To express a +range, omit the octet you want to wildcard and specify `'*'` in its +place. + +Examples of valid IPs include: + +* `"192.168.5.5"` — standard IPv4 +* `"192.168.*.*"` — range of IPv4 addresses; in the example, everything between `192.168.0.0` and `192.168.255.255` +* `"2001:db8:8714:3a90::12"` — standard IPv6; clustered empty ranges are fully supported +* `"2001:db8:8714:3a90:*:*"` — range of IPv6 addresses +* `"::ffff:192.0.2.128"` — IPv4 nested into IPv6 +* `"::192.0.2.128"` — IPv4 nested into IPv6 (without the `ffff` prefix) +* `"::ffff:192.0.*.*"` — ranges of IPv4 IPs nested into IPv6 + +### Allow-list mode + +By default (`ACCEPT` policy) the deny list is the exception list: use +`deny_ip` to refuse specific addresses and admit everyone else. To invert +this into an allow list — refuse everyone except specific addresses — set +the default policy to `REJECT` and populate the allow list with `allow_ip`: + +```cpp +webserver ws{create_webserver(8080) + .default_policy(http::http_utils::REJECT)}; +ws.allow_ip("192.168.0.*"); // permits 192.168.0.0/24, refuses everything else +``` + +See [`examples/minimal_ip_access_control.cpp`](examples/minimal_ip_access_control.cpp) +for a worked example of both modes. + +[Back to TOC](#table-of-contents) + +## Authentication + +libhttpserver supports four authentication mechanisms, all of which can +be combined freely (e.g. mTLS *and* digest auth in a two-factor +configuration): + +* **Basic authentication** — username and password are exchanged in + clear (base64-encoded) between the client and the server. Use only + for non-sensitive content or under HTTPS. With Basic auth, the + application sees the cleartext password — useful for chained + authentication against an external store. Enable / disable with + `basic_auth(true|false)` on the builder. +* **Digest authentication** — a one-way authentication scheme based on + hash algorithms (MD5, SHA-256, or SHA-512/256). Only the hash transits + the network; a server-issued nonce prevents replay attacks. Appropriate + for general use, especially when HTTPS is not available. SHA-256 is the + default; SHA-512/256 is also supported for stronger security. Enable / + disable with `digest_auth(true|false)`. +* **Client-certificate authentication (mTLS)** — uses an X.509 + certificate from the client. The strongest of the three mechanisms, + but requires HTTPS. Combines well with Basic or Digest for layered + authentication. Enable with `use_ssl(true)` plus configured `https_mem_trust`. +* **Centralized authentication** — a single per-request callback that + runs *before* any resource's render method, so individual handlers + don't need to repeat auth code. Configured via `auth_handler` and + `auth_skip_paths` on the builder. + +### Using Basic authentication + +```cpp +class user_pass_resource : public httpserver::http_resource { + public: + httpserver::http_response render_get(const httpserver::http_request& req) override { + if (req.get_user() != "myuser" || req.get_pass() != "mypass") { + return httpserver::http_response::unauthorized("test@example.com"); + } + return httpserver::http_response::string( + std::string{req.get_user()} + " " + std::string{req.get_pass()}); + } +}; + +int main() { + httpserver::webserver ws{httpserver::create_webserver(8080)}; + ws.register_path("/hello", std::make_unique()); + ws.start(true); +} +``` + +To test: + +```sh +curl -XGET -v -u myuser:mypass http://localhost:8080/hello +``` + +You will get back the user and password you passed. Try passing wrong +credentials to see the failure response. See +[`examples/basic_authentication.cpp`](examples/basic_authentication.cpp) +for the full source. + +### Using Digest authentication + +`check_digest_auth` returns a `digest_auth_result` enum with fine-grained +status codes (see [Request](#request) for the full list). The skeleton +of a digest-protected handler is: + +```cpp +httpserver::http_response render_get(const httpserver::http_request& req) override { + if (req.get_digested_user().empty()) { + return httpserver::http_response::unauthorized( + "test@example.com", 401, "text/plain", + httpserver::http::http_utils::digest_algorithm::SHA_256); + } + auto result = req.check_digest_auth( + "test@example.com", "mypass", 300, 0, + httpserver::http::http_utils::digest_algorithm::SHA_256); + if (result == httpserver::http::http_utils::digest_auth_result::NONCE_STALE) { + return httpserver::http_response::unauthorized( + "test@example.com", 401, "text/plain", + httpserver::http::http_utils::digest_algorithm::SHA_256) + .with_header("Stale", "true"); + } + if (result != httpserver::http::http_utils::digest_auth_result::OK) { + return httpserver::http_response::unauthorized( + "test@example.com", 401, "text/plain", + httpserver::http::http_utils::digest_algorithm::SHA_256); + } + return httpserver::http_response::string("SUCCESS"); +} +``` + +To test: + +```sh +curl -XGET -v --digest --user myuser:mypass localhost:8080/hello +``` + +You'll get `SUCCESS` in response; observe the response message in detail +to see the full digest handshake. See +[`examples/digest_authentication.cpp`](examples/digest_authentication.cpp). + +### Using centralized authentication + +The per-resource pattern above duplicates auth logic across every +resource. Centralized authentication lets you define the policy once and +have it applied automatically to every request: + +```cpp +// auth runs once per request before any render_*; return std::nullopt to allow +auto auth = [](const httpserver::http_request& req) + -> std::optional { + if (req.get_user() != "admin" || req.get_pass() != "secret") { + return httpserver::http_response::unauthorized("MyRealm"); + } + return std::nullopt; +}; + +httpserver::webserver ws{httpserver::create_webserver(8080) + .auth_handler(auth) + .auth_skip_paths({"/health", "/public/*"})}; + +ws.register_path("/api", std::make_unique()); +ws.register_path("/health", std::make_unique()); +ws.start(true); +``` + +The `auth_handler` callback runs for every request before the resource's +render method. It receives the `http_request` and can: + +* Return `std::nullopt` to allow the request to proceed normally. +* Return an `http_response` (typically `http_response::unauthorized`) to + reject the request. The response is moved onto the wire by the + dispatcher; no heap allocation is required. + +`auth_skip_paths` accepts a vector of paths that should bypass the +handler: + +* Exact matches: `"/health"` matches only `/health`. +* Wildcard suffixes: `"/public/*"` matches `/public/`, `/public/info`, + `/public/docs/api`, etc. + +To test (without auth — returns 401): + +```sh +curl -v http://localhost:8080/api +``` + +With valid auth — returns 200: + +```sh +curl -u admin:secret http://localhost:8080/api +``` + +Health endpoint (skip path) — works without auth: + +```sh +curl http://localhost:8080/health +``` + +See [`examples/centralized_authentication.cpp`](examples/centralized_authentication.cpp). + +### Using client-certificate authentication (mTLS) + +Client-certificate authentication (mutual TLS, mTLS) provides strong +authentication by requiring the client to present an X.509 certificate +during the TLS handshake. To enable mTLS: + +1. `use_ssl(true)` — enable TLS. +2. `https_mem_key("server_key.pem")` and `https_mem_cert("server_cert.pem")` — server certificate. +3. `https_mem_trust("ca_cert.pem")` — CA certificate(s) to verify client certificates. + +```cpp +class secure : public httpserver::http_resource { + public: + httpserver::http_response render_get(const httpserver::http_request& req) override { + if (!req.has_client_certificate()) { + return httpserver::http_response::string("client certificate required") + .with_status(401); + } + if (!req.is_client_cert_verified()) { + return httpserver::http_response::string("certificate not verified") + .with_status(403); + } + std::string cn{req.get_client_cert_cn()}; + return httpserver::http_response::string("Welcome, " + cn + "!"); + } +}; + +int main() { + httpserver::webserver ws{httpserver::create_webserver(8443) + .use_ssl(true) + .https_mem_key("server_key.pem") + .https_mem_cert("server_cert.pem") + .https_mem_trust("ca_cert.pem")}; + ws.register_path("/secure", std::make_unique()); + ws.start(true); +} +``` + +Available client-certificate methods (require GnuTLS support): + +* `has_client_certificate()` — was a client cert presented? +* `is_client_cert_verified()` — did the chain validate? +* `get_client_cert_dn()` — subject Distinguished Name +* `get_client_cert_issuer_dn()` — issuer Distinguished Name +* `get_client_cert_cn()` — Common Name from the subject +* `get_client_cert_fingerprint_sha256()` — hex-encoded SHA-256 fingerprint +* `get_client_cert_not_before()` / `get_client_cert_not_after()` — validity window + +Test with a client certificate: + +```sh +curl -k --cert client_cert.pem --key client_key.pem https://localhost:8443/secure +``` + +Without a client certificate (will be rejected): + +```sh +curl -k https://localhost:8443/secure +``` + +See [`examples/client_cert_auth.cpp`](examples/client_cert_auth.cpp). + +### Server Name Indication (SNI) callback + +SNI lets a server 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. +Configure an SNI callback that returns the `(cert_pem, key_pem)` pair for +each server name: + +```cpp +std::map> certs; + +auto sni = [](const std::string& server_name) + -> std::pair { + auto it = certs.find(server_name); + if (it != certs.end()) return it->second; + return {"", ""}; // fall back to the default certificate +}; + +httpserver::webserver ws{httpserver::create_webserver(443) + .use_ssl(true) + .https_mem_key("default_key.pem") + .https_mem_cert("default_cert.pem") + .sni_callback(sni)}; +``` + +SNI support requires libmicrohttpd 1.0.0 or later compiled with GnuTLS. + +### TLS-PSK (pre-shared key) + +For environments where X.509 certificates are impractical, libhttpserver +also supports TLS-PSK via `cred_type(http::http_utils::PSK)` plus a +`psk_cred_handler` callback returning the PSK for a given identity. See +[`examples/minimal_https_psk.cpp`](examples/minimal_https_psk.cpp). + +[Back to TOC](#table-of-contents) + +## WebSocket support + +libhttpserver provides WebSocket support when libmicrohttpd is built +with WebSocket functionality. To use WebSockets, derive from the +`websocket_handler` class and implement `on_message`. + +### The `websocket_handler` class + +The `websocket_handler` class provides the following virtual methods: + +* **`void on_open(websocket_session& session)`** — called when a new WebSocket connection is established. Default implementation does nothing. +* **`void on_message(websocket_session& session, std::string_view msg)`** — called when a text message is received. **This is the only pure virtual method and must be implemented.** +* **`void on_binary(websocket_session& session, const void* data, size_t len)`** — called when a binary message is received. Default implementation does nothing. +* **`void on_ping(websocket_session& session, std::string_view payload)`** — called when a ping frame is received. Default implementation sends a pong. +* **`void on_close(websocket_session& session, uint16_t code, const std::string& reason)`** — called when the WebSocket connection is closed. Default implementation does nothing. + +### The `websocket_session` class + +The `websocket_session` class provides methods to interact with the +client: + +* **`void send_text(const std::string& msg)`** — send a text message. +* **`void send_binary(const void* data, size_t len)`** — send a binary message. +* **`void send_ping(const std::string& payload = "")`** — send a ping frame. +* **`void send_pong(const std::string& payload = "")`** — send a pong frame. +* **`void close(uint16_t code = 1000, const std::string& reason = "")`** — close the WebSocket connection. +* **`bool is_valid()`** — true while the session is still open. + +### Registering WebSocket resources + +WebSocket handlers are registered with `register_ws_resource`, which +takes ownership of a `websocket_handler` subclass via `std::unique_ptr` +(or shares it via `std::shared_ptr`): + +```cpp +class echo : public httpserver::websocket_handler { + public: + void on_message(httpserver::websocket_session& session, + std::string_view msg) override { + session.send_text("Echo: " + std::string{msg}); + } +}; + +int main() { + httpserver::webserver ws{httpserver::create_webserver(8080)}; + ws.register_ws_resource("/echo", std::make_unique()); + // or, with shared ownership: + auto handler = std::make_shared(); + ws.register_ws_resource("/echo2", handler); + ws.start(true); +} +``` + +On a build with `HAVE_WEBSOCKET` disabled — for example, when the system +libmicrohttpd was built without WebSocket support — `register_ws_resource` +throws `feature_unavailable`. See [Feature +availability](#feature-availability) for how to detect this at runtime +without preprocessor gates, and +[`examples/websocket_echo.cpp`](examples/websocket_echo.cpp) for a +worked example. + +[Back to TOC](#table-of-contents) + +## Daemon introspection and external event loops + +libhttpserver exposes several methods for integrating with external +event loops and for querying daemon state at runtime. + +### Daemon introspection + +* **`uint16_t webserver::get_bound_port()`** — actual port the daemon is + bound to. Especially useful when you pass port `0` to let the OS + choose an ephemeral port. +* **`int webserver::get_listen_fd()`** — the listen socket file + descriptor, or `-1` if not available. +* **`unsigned int webserver::get_active_connections()`** — the number of + currently active connections. +* **`bool webserver::is_running()`** — true if the daemon is currently + accepting connections. + +### External event-loop integration + +When using the server without internal threading (e.g. with +`listen_socket(false)` or with `start_method(EXTERNAL_SELECT)`), you can +drive the event loop yourself: + +* **`bool webserver::run()`** — process pending events once and return + immediately. +* **`bool webserver::run_wait(int32_t millisec)`** — block until events + are available or the timeout expires. +* **`bool webserver::get_fdset(void* read, void* write, void* except, int* max_fd)`** + — retrieve file descriptor sets for use with `select(2)`. +* **`bool webserver::get_timeout(uint64_t* timeout)`** — the maximum + time to wait before calling `run()` again. +* **`bool webserver::add_connection(int socket, const sockaddr* addr, socklen_t len)`** + — hand off an externally-accepted connection to the daemon. Used with + socket-activation or load-balancer integrations. +* **`int webserver::quiesce()`** — stop accepting new connections while + allowing in-flight requests to complete. Returns the listen socket FD, + which the caller can then close or pass elsewhere. + +### Stopping the daemon + +* **`bool webserver::stop()`** — stop the daemon, join libmicrohttpd's + worker threads, and return. **Calling `stop()` from inside a handler + deadlocks** (it joins the calling thread); see [Threading + contract](#threading-contract). +* **`void webserver::stop_and_wait()`** — equivalent to `stop()` plus a + drain that waits for every in-flight handler to return. Same deadlock + contract as `stop()`. + +A small example combining everything: + +```cpp +// Bind to an OS-chosen port, print the actual one, drive the loop +// externally, then drain on shutdown. +webserver ws{create_webserver(0).start_method(http::http_utils::EXTERNAL_SELECT)}; +ws.start(false); +std::cout << "listening on port " << ws.get_bound_port() << '\n'; +while (running) { + ws.run_wait(1000); +} +ws.quiesce(); +ws.stop_and_wait(); +``` + +See [`examples/daemon_info.cpp`](examples/daemon_info.cpp) and +[`examples/external_event_loop.cpp`](examples/external_event_loop.cpp) +for runnable demonstrations. + +[Back to TOC](#table-of-contents) + +## HTTP utils + +libhttpserver provides a set of constants and helpers to help you build +your HTTP server. The full list of named constants (status codes, common +methods, common content types, named algorithms) lives in +[`src/httpserver/http_utils.hpp`](src/httpserver/http_utils.hpp) — it +would be redundant to enumerate it here. + +The following utility functions are available on `http::http_utils`: + +* **`const char* reason_phrase(unsigned int status_code)`** — the + standard HTTP reason phrase for a given status code (e.g. `"OK"` for + 200, `"Not Found"` for 404). +* **`bool is_feature_supported(int feature)`** — whether a specific + libmicrohttpd feature is supported on the current system. Feature + constants are defined by the `MHD_FEATURE` enum. +* **`const char* get_mhd_version()`** — the version string of the + underlying libmicrohttpd library. + +[Back to TOC](#table-of-contents) + +## Lifecycle hooks + +The hook bus is the single extension surface for observing or +short-circuiting the request lifecycle. It replaces v1's single-slot +patchwork (one `log_access` callback, one `not_found_handler`, one +`method_not_allowed_handler`, one `internal_error_handler`, one +`auth_handler`) with eleven distinct phases spanning connection, +request, routing, handler, and response. Each phase accepts multiple +subscribers and is observable both server-wide (via `webserver::add_hook`) +and, for the five post-route-resolution phases, per-route (via +`http_resource::add_hook`). The contract is captured in DR-012 and +[`specs/architecture/04-components/hooks.md`](specs/architecture/04-components/hooks.md). + +Each call returns a move-only `hook_handle`. The handle's destructor +removes the registration; `hook_handle::detach()` disarms the destructor +so the registration persists for the webserver's lifetime. + +### Phases + +| Phase | Fires at | Short-circuit | Per-route eligible | +|-------|----------|---------------|--------------------| +| `connection_opened` | New TCP / TLS connection accepted by MHD | No | No | +| `accept_decision` | After the default-policy / deny-list / allow-list verdict; the connection has been accepted or denied | No | No | +| `request_received` | Request line and headers parsed, body not yet consumed | Yes (`hook_action`) | No | +| `body_chunk` | Each upload-body chunk delivered by MHD | Yes (`hook_action`) | No | +| `route_resolved` | After URL → resource resolution; carries the matched route or "no match" | No | No | +| `before_handler` | After route resolution and method check, immediately before the handler runs | Yes (`hook_action`) | Yes | +| `handler_exception` | Exception escapes the handler, before `internal_error_handler` is consulted | Yes (`hook_action`) | Yes | +| `after_handler` | Handler returned a response, before it is queued on the wire (mutation point) | Yes (`hook_action`) | Yes | +| `response_sent` | Response handed to `MHD_queue_response`; carries `status`, `bytes_queued`, `elapsed` | No | Yes | +| `request_completed` | Request lifecycle finished (success or failure); last hook to fire per request | No | Yes | +| `connection_closed` | Connection torn down (peer close or server close) | No | No | + +### Short-circuit semantics + +Phases marked "Short-circuit" return a `hook_action`: +`hook_action::pass()` lets the chain continue; +`hook_action::respond_with(response)` aborts the chain at that +position. The wrapped response is sent on the wire in place of any +handler output. Subsequent hooks in the same phase are not invoked. +Observation-only phases (`connection_opened`, `accept_decision`, +`connection_closed`, `route_resolved`, `response_sent`, +`request_completed`) ignore the return; the chain always runs to +completion. + +### Per-route hooks + +`http_resource::add_hook(phase, fn)` accepts only the five +post-route-resolution phases: `before_handler`, `handler_exception`, +`after_handler`, `response_sent`, `request_completed`. Per-route hooks +fire after the server-wide chain at the same phase, and only if that +server-wide chain did not short-circuit. Passing any other phase +throws `std::invalid_argument` naming the rejected phase. See +[`examples/per_route_auth.cpp`](examples/per_route_auth.cpp) for a +worked example. + +### Examples + +* [`examples/denied_ip_log.cpp`](examples/denied_ip_log.cpp) — log every + denied-IP rejection via a single `accept_decision` hook. +* [`examples/early_413.cpp`](examples/early_413.cpp) — return 413 from + a `request_received` hook before the body is consumed. +* [`examples/clf_access_log.cpp`](examples/clf_access_log.cpp) — write + a Common Log Format access line from a `response_sent` hook. +* [`examples/per_route_auth.cpp`](examples/per_route_auth.cpp) — + per-route HTTP Basic auth via `http_resource::add_hook(before_handler, ...)`. + +### v1 aliases + +Each of the v1 single-slot setters is an alias for an `add_hook` call +at the corresponding phase. The aliases survive for ergonomic call +sites; new code can use either form. + +| v1 setter | Equivalent `add_hook` call | +|-----------|----------------------------| +| `log_access(fn)` | `ws.add_hook(hook_phase::response_sent, fn)` | +| `not_found_handler(fn)` | `ws.add_hook(hook_phase::route_resolved, fn)` | +| `method_not_allowed_handler(fn)` | `ws.add_hook(hook_phase::before_handler, fn)` | +| `internal_error_handler(fn)` | `ws.add_hook(hook_phase::handler_exception, fn)` | +| `auth_handler(fn)` | `ws.add_hook(hook_phase::before_handler, fn)` | + +The aliases install observation-stub hooks under the same dispatch +plumbing, so the on-the-wire behaviour is identical regardless of +which form the caller used. + +[Back to TOC](#table-of-contents) + +## Threading contract + +Distilled from +[`specs/architecture/05-cross-cutting.md`](specs/architecture/05-cross-cutting.md) +§5.1 and DR-008 +([`specs/architecture/11-decisions/DR-008.md`](specs/architecture/11-decisions/DR-008.md)): + +1. **Public methods on `webserver` are thread-safe and re-entrant from + inside a handler.** Two exceptions: `stop()` and `~webserver()` + **deadlock** if invoked from a handler thread, because they wait for + that very thread to drain. `stop_and_wait()` delegates to `stop()` + and carries the same deadlock risk. Stop the server from a different + thread, or signal an external stop loop. v2 provides both `stop()` + (signal and return) and `stop_and_wait()` (signal and drain until + all in-flight handlers finish). +2. **Handlers run concurrently on libmicrohttpd worker threads.** The + same lambda or `http_resource` instance is invoked from many threads + at once. Any state you share — counters, caches, file handles — must + be synchronised on your side. The library does not synchronise user + state for you. +3. **`http_request` is single-threaded per request.** Sharing one + `http_request` across threads is undefined; the per-request arena + makes no guarantees outside the calling thread. +4. **`http_response` is value-typed with exclusive ownership.** + Returning it transfers ownership into the dispatcher. There is no + shared mutable response object. +5. **User-supplied callbacks** — `log_access`, `log_error`, + `not_found_handler`, `method_not_allowed_handler`, + `internal_error_handler`, `file_cleanup_callback`, PSK / SNI / ALPN + callbacks, and any registered `render_*` — may run concurrently on + multiple threads. Implementations MUST be thread-safe. + +[Back to TOC](#table-of-contents) + +## Error propagation + +Distilled from +[`specs/architecture/05-cross-cutting.md`](specs/architecture/05-cross-cutting.md) +§5.2 and DR-009 +([`specs/architecture/11-decisions/DR-009.md`](specs/architecture/11-decisions/DR-009.md)): + +1. **A handler that throws `std::exception` is caught.** The library + logs the exception via the configured `log_error` callback and then + invokes `internal_error_handler(request, e.what())`. The response + returned by `internal_error_handler` is sent to the client; if no + custom `internal_error_handler` is configured, a default 500 with + the **fixed body `"Internal Server Error"`** is sent + (DR-009 Revision 1, CWE-209 fix — exception text often contains + file paths, SQL fragments, or attacker-influenced input and must + not cross a process boundary to an untrusted client). To restore + the v1 behaviour of including the exception message in the body + for development, set `.expose_exception_messages(true)` on the + builder. The verbatim message is still surfaced via the + `log_error` callback and to any configured + `internal_error_handler` regardless of the flag. +2. **A handler that throws something other than `std::exception`** is + also caught, with `"unknown exception"` substituted for the message. + The same default body (`"Internal Server Error"`) applies; the + `"unknown exception"` sentinel reaches the wire only when + `.expose_exception_messages(true)` is set. +3. **Library-internal failures during dispatch** (allocation, body + materialisation) flow through the same `internal_error_handler` path. +4. **If `internal_error_handler` itself throws**, the library logs and + sends a hardcoded 500 with an empty body. There is no third level of + fallback. +5. **`feature_unavailable` is a normal `std::runtime_error`** — no + special status mapping. Catch it explicitly if you want to map it to + a 503 or similar; the library does not. +6. **There is no throw-as-status idiom.** A handler that wants to + return 404, 400, etc. builds the response by value (see + [Response](#response)): + `return httpserver::http_response::empty().with_status(404);`. + +Install custom error handlers on the builder: + +```cpp +auto cfg = httpserver::create_webserver(8080) + .not_found_handler([](const httpserver::http_request&) { + return httpserver::http_response::string("nope").with_status(404); + }) + .method_not_allowed_handler([](const httpserver::http_request&) { + return httpserver::http_response::empty().with_status(405); + }) + .internal_error_handler([](const httpserver::http_request&, std::string_view what) { + // CWE-209: 'what' may contain file paths, SQL fragments, or + // attacker-influenced input. Log it internally; do NOT echo + // it to the HTTP client. + (void)what; // e.g. logger->error(what); + return httpserver::http_response::string("Internal Server Error").with_status(500); + }); +httpserver::webserver ws{cfg}; +``` + +See [`examples/custom_error.cpp`](examples/custom_error.cpp) for a +worked example. Note that the snippet above — which echoes `what` +verbatim to the wire — is illustrative of the explicit-handler case; +the **default** (no `internal_error_handler` configured) path now +sends a fixed body, and only the `log_error` callback receives the +verbatim message. To restore the v1 verbose-body default for +development, chain `.expose_exception_messages(true)` on the builder +(see [Custom error handlers](#custom-error-handlers)). + +[Back to TOC](#table-of-contents) + +## Feature availability + +Several capabilities are gated by build-time flags. v2.0 makes the +gating visible at the API level so application code does not need +preprocessor guards on `HAVE_*` macros. + +| Build flag | When disabled | Public-API behavior | +|---|---|---| +| `HAVE_BAUTH` | Basic-auth disabled | `get_user`, `get_pass` return empty `string_view`; `features().basic_auth == false`; `create_webserver::basic_auth(true)` throws `feature_unavailable` at `webserver` construction | +| `HAVE_DAUTH` | Digest-auth disabled | `get_digested_user` returns empty; `check_digest_auth` returns a sentinel result; `features().digest_auth == false`; `create_webserver::digest_auth(true)` throws `feature_unavailable` | +| `HAVE_GNUTLS` | TLS disabled | All `get_client_cert_*` accessors return empty / `-1` / `false`; `features().tls == false`; `create_webserver::use_ssl(true)` throws `feature_unavailable` | +| `HAVE_WEBSOCKET` | WebSocket disabled | `register_ws_resource` throws `feature_unavailable`; `features().websocket == false` | + +### Probing at runtime + +`webserver::features()` returns a small struct of four `bool`s — one +per flag — so callers can branch without preprocessor help: + +```cpp +if (ws.features().tls) { + // safe to read client-certificate accessors on this build +} +if (ws.features().websocket) { + ws.register_ws_resource("/sock", std::make_unique()); +} +``` + +### `feature_unavailable` + +Derives from `std::runtime_error`. Its `what()` names both the disabled +feature and the build flag that gates it, so log lines pinpoint which +flag a deployment is missing. Catch it where you call a feature-gated +method: + +```cpp +try { + ws.register_ws_resource("/sock", std::make_unique()); +} catch (const httpserver::feature_unavailable& e) { + std::cerr << "websocket support is not available: " << e.what() << '\n'; +} +``` + +Deny lists, IP-allow handling, and similar features that do not depend +on external libraries are always available: `webserver::deny_ip(addr)` / +`webserver::remove_denied_ip(addr)` and `webserver::allow_ip(addr)` / +`webserver::remove_allowed_ip(addr)` install and clear per-server access +rules at runtime regardless of build flags. + +[Back to TOC](#table-of-contents) + +## Migrating from v1 + +If you're porting an application from the v1 line, the rename / removed / +added cheat sheet — every API surface that moved — lives in +[`RELEASE_NOTES.md`](RELEASE_NOTES.md). The packaging is +parallel-installable across major versions, so applications can link +the major they need side by side while porting. + +[Back to TOC](#table-of-contents) + +## Examples + +Every example is a standalone program under [`examples/`](examples/). A +fully grouped index — with one-line descriptions of every binary — lives +in [`examples/README.md`](examples/README.md). The summary below points +to the canonical example for each topic covered in this manual. + +### Start here + +* [`examples/hello_world.cpp`](examples/hello_world.cpp) — ten lines, lambda + form, no subclass, no raw pointer. Reproduced byte-for-byte at the top + of this README. +* [`examples/shared_state.cpp`](examples/shared_state.cpp) — when the + class form is the right shape: an `http_resource` subclass with a + mutex-guarded counter shared between `render_get` and `render_post`. + +### HTTP basics + +* [`examples/setting_headers.cpp`](examples/setting_headers.cpp) — fluent + `with_header` chaining. +* [`examples/hello_with_get_arg.cpp`](examples/hello_with_get_arg.cpp) — + read a query-string parameter from `http_request::get_arg`. +* [`examples/args_processing.cpp`](examples/args_processing.cpp) — + iterate every form / query argument. +* [`examples/url_registration.cpp`](examples/url_registration.cpp) — + exact paths, prefix matches, parameterised segments, per-segment regex. +* [`examples/handlers.cpp`](examples/handlers.cpp) — distinct lambdas + for GET and POST on the same path; the dispatcher composes them. +* [`examples/allowing_disallowing_methods.cpp`](examples/allowing_disallowing_methods.cpp) + — narrow a resource to a specific subset of HTTP verbs. +* [`examples/custom_error.cpp`](examples/custom_error.cpp) — install + `not_found_handler`, `method_not_allowed_handler`, and `internal_error_handler`. + +### Response shapes + +* [`examples/minimal_file_response.cpp`](examples/minimal_file_response.cpp) — + stream a file from disk via `http_response::file`. +* [`examples/binary_buffer_response.cpp`](examples/binary_buffer_response.cpp) — + return a binary buffer (e.g. a PNG) from memory. +* [`examples/iovec_response_example.cpp`](examples/iovec_response_example.cpp) — + gather a response body from multiple borrowed buffers without copying. +* [`examples/pipe_response_example.cpp`](examples/pipe_response_example.cpp) — + stream from a pipe filled by a background thread. +* [`examples/empty_response_example.cpp`](examples/empty_response_example.cpp) — + bodyless responses for DELETE and HEAD. +* [`examples/minimal_deferred.cpp`](examples/minimal_deferred.cpp) — body + produced asynchronously by a callback. +* [`examples/deferred_with_accumulator.cpp`](examples/deferred_with_accumulator.cpp) — + deferred response that mutates shared state across calls. + +### TLS and authentication + +* [`examples/minimal_https.cpp`](examples/minimal_https.cpp) — enable + TLS with PEM key / cert files. +* [`examples/minimal_https_psk.cpp`](examples/minimal_https_psk.cpp) — + TLS with pre-shared keys. +* [`examples/basic_authentication.cpp`](examples/basic_authentication.cpp) — + per-request HTTP Basic auth inside the handler. +* [`examples/centralized_authentication.cpp`](examples/centralized_authentication.cpp) — + server-wide `auth_handler` and `auth_skip_paths`. +* [`examples/digest_authentication.cpp`](examples/digest_authentication.cpp) — + per-request HTTP Digest auth via `check_digest_auth`. +* [`examples/client_cert_auth.cpp`](examples/client_cert_auth.cpp) — + mutual TLS with X.509 client certificates. + +### Lifecycle hooks + +* [`examples/denied_ip_log.cpp`](examples/denied_ip_log.cpp) — log every + denied-IP rejection from a single `accept_decision` hook (issue #332). +* [`examples/early_413.cpp`](examples/early_413.cpp) — short-circuit + oversized uploads with 413 before any body bytes are consumed via a + `request_received` hook (issue #273). +* [`examples/clf_access_log.cpp`](examples/clf_access_log.cpp) — + Common Log Format access logger written as a `response_sent` hook + using the structured `status` / `bytes_queued` / `elapsed` context + fields (issues #281 and #69). +* [`examples/per_route_auth.cpp`](examples/per_route_auth.cpp) — + per-route HTTP Basic auth via `http_resource::add_hook(before_handler, + ...)` that does not touch sibling routes (DR-012). + +### Operations + +* [`examples/daemon_info.cpp`](examples/daemon_info.cpp) — introspect + the running daemon (bound port, listen FD, active connections). +* [`examples/external_event_loop.cpp`](examples/external_event_loop.cpp) — + drive `EXTERNAL_SELECT` from the application's loop via `run_wait`. +* [`examples/custom_access_log.cpp`](examples/custom_access_log.cpp) — + server-wide access-log callback. +* [`examples/minimal_ip_access_control.cpp`](examples/minimal_ip_access_control.cpp) — + `deny_ip` / `allow_ip` under the `ACCEPT` and `REJECT` policies. +* [`examples/turbo_mode.cpp`](examples/turbo_mode.cpp) — turbo, + suppressed Date header, fastopen queue, listen backlog. +* [`examples/service.cpp`](examples/service.cpp) — kitchen-sink + reference example: CLI args, optional TLS, every `render_*` override. + +### File uploads + +* [`examples/file_upload.cpp`](examples/file_upload.cpp) — multipart + upload through `http_request::get_files`. +* [`examples/file_upload_with_callback.cpp`](examples/file_upload_with_callback.cpp) — + upload with a `file_cleanup_callback`. + +### WebSockets + +* [`examples/websocket_echo.cpp`](examples/websocket_echo.cpp) — + `websocket_handler` subclass registered via `register_ws_resource`. + +### Benchmarks + +* [`examples/benchmark_select.cpp`](examples/benchmark_select.cpp), + [`examples/benchmark_threads.cpp`](examples/benchmark_threads.cpp), + [`examples/benchmark_nodelay.cpp`](examples/benchmark_nodelay.cpp) — + micro-benchmarks. + +[Back to TOC](#table-of-contents) + +## Copying + +This manual is for libhttpserver, a C++ library for creating an embedded +RESTful HTTP server (and more). > Permission is granted to copy, distribute and/or modify this document > under the terms of the GNU Free Documentation License, Version 1.3 > or any later version published by the Free Software Foundation; > with no Invariant Sections, no Front-Cover Texts, and no Back-Cover -> Texts. A copy of the license is included in the section entitled GNU -> Free Documentation License. - -Contents -======== -* Introduction. -* Requirements. -* Compilation. -* Constants. -* Structures and classes type definition. -* Callback functions definition. -* Create and work with server. -* Registering resources. -* Building responses to requests. -* Whitelists and Blacklists. -* Simple comet semantics. -* Utilizing Authentication. -* Obtaining and modifying status information. - -Appendices ----------- -* GNU-LGPL: The GNU Lesser General Public License says how you can copy and share almost all of libhttpserver. -* GNU-FDL: The GNU Free Documentation License says how you can copy and share the documentation of libhttpserver. - -Introduction -============ -libhttpserver is meant to constitute an easy system to build HTTP -servers with REST fashion. -libhttpserver is based on libmicrohttpd and, like this, it is a -daemon library. -The mission of this library is to support all possible HTTP features -directly and with a simple semantic allowing then the user to concentrate -only on his application and not on HTTP request handling details. - -The library is supposed to work transparently for the client Implementing -the business logic and using the library itself to realize an interface. -If the user wants it must be able to change every behavior of the library -itself through the registration of callbacks. - -Like the api is based on (libmicrohttpd), libhttpserver is able to decode -certain body format a and automatically format them in object oriented -fashion. This is true for query arguments and for *POST* and *PUT* -requests bodies if *application/x-www-form-urlencoded* or -*multipart/form-data* header are passed. - -The header reproduce all the constants defined by libhttpserver. -These maps various constant used by the HTTP protocol that are exported -as a convenience for users of the library. Is is possible for the user -to define their own extensions of the HTTP standard and use those with -libhttpserver. - -All functions are guaranteed to be completely reentrant and -thread-safe (unless differently specified). -Additionally, clients can specify resource limits on the overall -number of connections, number of connections per IP address and memory -used per connection to avoid resource exhaustion. - -Requirements -============ -* g++ >= 4.1.2 -* libmicrohttpd >= 0.9.37 -* doxygen (if you want to build code reference) - -Additionally, for MinGW on windows you will need: -* libwinpthread (For MinGW-w64, if you use thread model posix then you have this) -* libgnurx >= 2.5.1 +> Texts. A copy of the license is included in the file [`LICENSE`](LICENSE). -Compilation -=========== -libhttpserver uses the standard system where the usual build process -involves running -> ./bootstrap -> mkdir build -> cd build -> ../configure -> make -> make install -> make doxygen-doc # if you want to build the code reference - -Optional parameters to configure script ---------------------------------------- -A complete list of parameters can be obtained running 'configure --help'. -Here are listed the libhttpserver specific options (the canonical configure options are also supported). - -* --enable-same-directory-build: enable to compile in the same directory. This is heavily discouraged. (def=no) -* --enable-debug: enable debug data generation (def=no) -* --enable-cpp11: enable c++11 std classes (def=no) -* --disable-doxygen-doc: don't generate any doxygen documentation -* --disable-doxygen-dot: don't generate graphics for doxygen documentation -* --disable-doxygen-man: don't generate doxygen manual pages -* --enable-doxygen-rtf: generate doxygen RTF documentation -* --enable-doxygen-xml: generate doxygen XML documentation -* --enable-doxygen-chm: generate doxygen compressed HTML help documentation -* --enable-doxygen-chi: generate doxygen seperate compressed HTML help index file -* --disable-doxygen-html: don't generate doxygen plain HTML documentation -* --enable-doxygen-ps: generate doxygen PostScript documentation -* --enable-doxygen-pdf: generate doxygen PDF documentation - -Constants -========= -W.I.P. - -Structures and classes type definition -====================================== -* http_resource (CPP class): Represents the resource associated with a specific http endpoint. -* http_request (CPP class): Represents the request received by the resource that process it. -* http_response (CPP class): Represents the response sent by the server once the resource finished its work. -* event_supplier (CPP class): Represents a class that supplies events to the webserver. It can be used to trigger the internal select of it. -* webserver (CPP class): Represents the daemon listening on a socket for HTTP traffic. - -GNU Lesser General Public License -================================= - -Version 2.1, February 1999 - -Copyright © 1991, 1999 Free Software Foundation, Inc. -51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -Everyone is permitted to copy and distribute verbatim copies -of this license document, but changing it is not allowed. - -_This is the first released version of the Lesser GPL. It also counts -as the successor of the GNU Library Public License, version 2, hence -the version number 2.1._ - -### Preamble - -The licenses for most software are designed to take away your -freedom to share and change it. By contrast, the GNU General Public -Licenses are intended to guarantee your freedom to share and change -free software--to make sure the software is free for all its users. - -This license, the Lesser General Public License, applies to some -specially designated software packages--typically libraries--of the -Free Software Foundation and other authors who decide to use it. You -can use it too, but we suggest you first think carefully about whether -this license or the ordinary General Public License is the better -strategy to use in any particular case, based on the explanations below. - -When we speak of free software, we are referring to freedom of use, -not price. Our General Public Licenses are designed to make sure that -you have the freedom to distribute copies of free software (and charge -for this service if you wish); that you receive source code or can get -it if you want it; that you can change the software and use pieces of -it in new free programs; and that you are informed that you can do -these things. - -To protect your rights, we need to make restrictions that forbid -distributors to deny you these rights or to ask you to surrender these -rights. These restrictions translate to certain responsibilities for -you if you distribute copies of the library or if you modify it. - -For example, if you distribute copies of the library, whether gratis -or for a fee, you must give the recipients all the rights that we gave -you. You must make sure that they, too, receive or can get the source -code. If you link other code with the library, you must provide -complete object files to the recipients, so that they can relink them -with the library after making changes to the library and recompiling -it. And you must show them these terms so they know their rights. - -We protect your rights with a two-step method: (1) we copyright the -library, and (2) we offer you this license, which gives you legal -permission to copy, distribute and/or modify the library. - -To protect each distributor, we want to make it very clear that -there is no warranty for the free library. Also, if the library is -modified by someone else and passed on, the recipients should know -that what they have is not the original version, so that the original -author's reputation will not be affected by problems that might be -introduced by others. - -Finally, software patents pose a constant threat to the existence of -any free program. We wish to make sure that a company cannot -effectively restrict the users of a free program by obtaining a -restrictive license from a patent holder. Therefore, we insist that -any patent license obtained for a version of the library must be -consistent with the full freedom of use specified in this license. - -Most GNU software, including some libraries, is covered by the -ordinary GNU General Public License. This license, the GNU Lesser -General Public License, applies to certain designated libraries, and -is quite different from the ordinary General Public License. We use -this license for certain libraries in order to permit linking those -libraries into non-free programs. - -When a program is linked with a library, whether statically or using -a shared library, the combination of the two is legally speaking a -combined work, a derivative of the original library. The ordinary -General Public License therefore permits such linking only if the -entire combination fits its criteria of freedom. The Lesser General -Public License permits more lax criteria for linking other code with -the library. - -We call this license the “Lesser” General Public License because it -does Less to protect the user's freedom than the ordinary General -Public License. It also provides other free software developers Less -of an advantage over competing non-free programs. These disadvantages -are the reason we use the ordinary General Public License for many -libraries. However, the Lesser license provides advantages in certain -special circumstances. - -For example, on rare occasions, there may be a special need to -encourage the widest possible use of a certain library, so that it becomes -a de-facto standard. To achieve this, non-free programs must be -allowed to use the library. A more frequent case is that a free -library does the same job as widely used non-free libraries. In this -case, there is little to gain by limiting the free library to free -software only, so we use the Lesser General Public License. - -In other cases, permission to use a particular library in non-free -programs enables a greater number of people to use a large body of -free software. For example, permission to use the GNU C Library in -non-free programs enables many more people to use the whole GNU -operating system, as well as its variant, the GNU/Linux operating -system. - -Although the Lesser General Public License is Less protective of the -users' freedom, it does ensure that the user of a program that is -linked with the Library has the freedom and the wherewithal to run -that program using a modified version of the Library. - -The precise terms and conditions for copying, distribution and -modification follow. Pay close attention to the difference between a -“work based on the library” and a “work that uses the library”. The -former contains code derived from the library, whereas the latter must -be combined with the library in order to run. - -### TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - -**0.** This License Agreement applies to any software library or other -program which contains a notice placed by the copyright holder or -other authorized party saying it may be distributed under the terms of -this Lesser General Public License (also called “this License”). -Each licensee is addressed as “you”. - -A “library” means a collection of software functions and/or data -prepared so as to be conveniently linked with application programs -(which use some of those functions and data) to form executables. - -The “Library”, below, refers to any such software library or work -which has been distributed under these terms. A “work based on the -Library” means either the Library or any derivative work under -copyright law: that is to say, a work containing the Library or a -portion of it, either verbatim or with modifications and/or translated -straightforwardly into another language. (Hereinafter, translation is -included without limitation in the term “modification”.) - -“Source code” for a work means the preferred form of the work for -making modifications to it. For a library, complete source code means -all the source code for all modules it contains, plus any associated -interface definition files, plus the scripts used to control compilation -and installation of the library. - -Activities other than copying, distribution and modification are not -covered by this License; they are outside its scope. The act of -running a program using the Library is not restricted, and output from -such a program is covered only if its contents constitute a work based -on the Library (independent of the use of the Library in a tool for -writing it). Whether that is true depends on what the Library does -and what the program that uses the Library does. - -**1.** You may copy and distribute verbatim copies of the Library's -complete source code as you receive it, in any medium, provided that -you conspicuously and appropriately publish on each copy an -appropriate copyright notice and disclaimer of warranty; keep intact -all the notices that refer to this License and to the absence of any -warranty; and distribute a copy of this License along with the -Library. - -You may charge a fee for the physical act of transferring a copy, -and you may at your option offer warranty protection in exchange for a -fee. - -**2.** You may modify your copy or copies of the Library or any portion -of it, thus forming a work based on the Library, and copy and -distribute such modifications or work under the terms of Section 1 -above, provided that you also meet all of these conditions: - -* **a)** The modified work must itself be a software library. -* **b)** You must cause the files modified to carry prominent notices -stating that you changed the files and the date of any change. -* **c)** You must cause the whole of the work to be licensed at no -charge to all third parties under the terms of this License. -* **d)** If a facility in the modified Library refers to a function or a -table of data to be supplied by an application program that uses -the facility, other than as an argument passed when the facility -is invoked, then you must make a good faith effort to ensure that, -in the event an application does not supply such function or -table, the facility still operates, and performs whatever part of -its purpose remains meaningful. -(For example, a function in a library to compute square roots has -a purpose that is entirely well-defined independent of the -application. Therefore, Subsection 2d requires that any -application-supplied function or table used by this function must -be optional: if the application does not supply it, the square -root function must still compute square roots.) - -These requirements apply to the modified work as a whole. If -identifiable sections of that work are not derived from the Library, -and can be reasonably considered independent and separate works in -themselves, then this License, and its terms, do not apply to those -sections when you distribute them as separate works. But when you -distribute the same sections as part of a whole which is a work based -on the Library, the distribution of the whole must be on the terms of -this License, whose permissions for other licensees extend to the -entire whole, and thus to each and every part regardless of who wrote -it. - -Thus, it is not the intent of this section to claim rights or contest -your rights to work written entirely by you; rather, the intent is to -exercise the right to control the distribution of derivative or -collective works based on the Library. - -In addition, mere aggregation of another work not based on the Library -with the Library (or with a work based on the Library) on a volume of -a storage or distribution medium does not bring the other work under -the scope of this License. - -**3.** You may opt to apply the terms of the ordinary GNU General Public -License instead of this License to a given copy of the Library. To do -this, you must alter all the notices that refer to this License, so -that they refer to the ordinary GNU General Public License, version 2, -instead of to this License. (If a newer version than version 2 of the -ordinary GNU General Public License has appeared, then you can specify -that version instead if you wish.) Do not make any other change in -these notices. - -Once this change is made in a given copy, it is irreversible for -that copy, so the ordinary GNU General Public License applies to all -subsequent copies and derivative works made from that copy. - -This option is useful when you wish to copy part of the code of -the Library into a program that is not a library. - -**4.** You may copy and distribute the Library (or a portion or -derivative of it, under Section 2) in object code or executable form -under the terms of Sections 1 and 2 above provided that you accompany -it with the complete corresponding machine-readable source code, which -must be distributed under the terms of Sections 1 and 2 above on a -medium customarily used for software interchange. - -If distribution of object code is made by offering access to copy -from a designated place, then offering equivalent access to copy the -source code from the same place satisfies the requirement to -distribute the source code, even though third parties are not -compelled to copy the source along with the object code. - -**5.** A program that contains no derivative of any portion of the -Library, but is designed to work with the Library by being compiled or -linked with it, is called a “work that uses the Library”. Such a -work, in isolation, is not a derivative work of the Library, and -therefore falls outside the scope of this License. - -However, linking a “work that uses the Library” with the Library -creates an executable that is a derivative of the Library (because it -contains portions of the Library), rather than a “work that uses the -library”. The executable is therefore covered by this License. -Section 6 states terms for distribution of such executables. - -When a “work that uses the Library” uses material from a header file -that is part of the Library, the object code for the work may be a -derivative work of the Library even though the source code is not. -Whether this is true is especially significant if the work can be -linked without the Library, or if the work is itself a library. The -threshold for this to be true is not precisely defined by law. - -If such an object file uses only numerical parameters, data -structure layouts and accessors, and small macros and small inline -functions (ten lines or less in length), then the use of the object -file is unrestricted, regardless of whether it is legally a derivative -work. (Executables containing this object code plus portions of the -Library will still fall under Section 6.) - -Otherwise, if the work is a derivative of the Library, you may -distribute the object code for the work under the terms of Section 6. -Any executables containing that work also fall under Section 6, -whether or not they are linked directly with the Library itself. - -**6.** As an exception to the Sections above, you may also combine or -link a “work that uses the Library” with the Library to produce a -work containing portions of the Library, and distribute that work -under terms of your choice, provided that the terms permit -modification of the work for the customer's own use and reverse -engineering for debugging such modifications. - -You must give prominent notice with each copy of the work that the -Library is used in it and that the Library and its use are covered by -this License. You must supply a copy of this License. If the work -during execution displays copyright notices, you must include the -copyright notice for the Library among them, as well as a reference -directing the user to the copy of this License. Also, you must do one -of these things: - -* **a)** Accompany the work with the complete corresponding -machine-readable source code for the Library including whatever -changes were used in the work (which must be distributed under -Sections 1 and 2 above); and, if the work is an executable linked -with the Library, with the complete machine-readable “work that -uses the Library”, as object code and/or source code, so that the -user can modify the Library and then relink to produce a modified -executable containing the modified Library. (It is understood -that the user who changes the contents of definitions files in the -Library will not necessarily be able to recompile the application -to use the modified definitions.) -* **b)** Use a suitable shared library mechanism for linking with the -Library. A suitable mechanism is one that (1) uses at run time a -copy of the library already present on the user's computer system, -rather than copying library functions into the executable, and (2) -will operate properly with a modified version of the library, if -the user installs one, as long as the modified version is -interface-compatible with the version that the work was made with. -* **c)** Accompany the work with a written offer, valid for at -least three years, to give the same user the materials -specified in Subsection 6a, above, for a charge no more -than the cost of performing this distribution. -* **d)** If distribution of the work is made by offering access to copy -from a designated place, offer equivalent access to copy the above -specified materials from the same place. -* **e)** Verify that the user has already received a copy of these -materials or that you have already sent this user a copy. - -For an executable, the required form of the “work that uses the -Library” must include any data and utility programs needed for -reproducing the executable from it. However, as a special exception, -the materials to be distributed need not include anything that is -normally distributed (in either source or binary form) with the major -components (compiler, kernel, and so on) of the operating system on -which the executable runs, unless that component itself accompanies -the executable. - -It may happen that this requirement contradicts the license -restrictions of other proprietary libraries that do not normally -accompany the operating system. Such a contradiction means you cannot -use both them and the Library together in an executable that you -distribute. - -**7.** You may place library facilities that are a work based on the -Library side-by-side in a single library together with other library -facilities not covered by this License, and distribute such a combined -library, provided that the separate distribution of the work based on -the Library and of the other library facilities is otherwise -permitted, and provided that you do these two things: - -* **a)** Accompany the combined library with a copy of the same work -based on the Library, uncombined with any other library -facilities. This must be distributed under the terms of the -Sections above. -* **b)** Give prominent notice with the combined library of the fact -that part of it is a work based on the Library, and explaining -where to find the accompanying uncombined form of the same work. - -**8.** You may not copy, modify, sublicense, link with, or distribute -the Library except as expressly provided under this License. Any -attempt otherwise to copy, modify, sublicense, link with, or -distribute the Library is void, and will automatically terminate your -rights under this License. However, parties who have received copies, -or rights, from you under this License will not have their licenses -terminated so long as such parties remain in full compliance. - -**9.** You are not required to accept this License, since you have not -signed it. However, nothing else grants you permission to modify or -distribute the Library or its derivative works. These actions are -prohibited by law if you do not accept this License. Therefore, by -modifying or distributing the Library (or any work based on the -Library), you indicate your acceptance of this License to do so, and -all its terms and conditions for copying, distributing or modifying -the Library or works based on it. - -**10.** Each time you redistribute the Library (or any work based on the -Library), the recipient automatically receives a license from the -original licensor to copy, distribute, link with or modify the Library -subject to these terms and conditions. You may not impose any further -restrictions on the recipients' exercise of the rights granted herein. -You are not responsible for enforcing compliance by third parties with -this License. - -**11.** If, as a consequence of a court judgment or allegation of patent -infringement or for any other reason (not limited to patent issues), -conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot -distribute so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you -may not distribute the Library at all. For example, if a patent -license would not permit royalty-free redistribution of the Library by -all those who receive copies directly or indirectly through you, then -the only way you could satisfy both it and this License would be to -refrain entirely from distribution of the Library. - -If any portion of this section is held invalid or unenforceable under any -particular circumstance, the balance of the section is intended to apply, -and the section as a whole is intended to apply in other circumstances. - -It is not the purpose of this section to induce you to infringe any -patents or other property right claims or to contest validity of any -such claims; this section has the sole purpose of protecting the -integrity of the free software distribution system which is -implemented by public license practices. Many people have made -generous contributions to the wide range of software distributed -through that system in reliance on consistent application of that -system; it is up to the author/donor to decide if he or she is willing -to distribute software through any other system and a licensee cannot -impose that choice. - -This section is intended to make thoroughly clear what is believed to -be a consequence of the rest of this License. - -**12.** If the distribution and/or use of the Library is restricted in -certain countries either by patents or by copyrighted interfaces, the -original copyright holder who places the Library under this License may add -an explicit geographical distribution limitation excluding those countries, -so that distribution is permitted only in or among countries not thus -excluded. In such case, this License incorporates the limitation as if -written in the body of this License. - -**13.** The Free Software Foundation may publish revised and/or new -versions of the Lesser General Public License from time to time. -Such new versions will be similar in spirit to the present version, -but may differ in detail to address new problems or concerns. - -Each version is given a distinguishing version number. If the Library -specifies a version number of this License which applies to it and -“any later version”, you have the option of following the terms and -conditions either of that version or of any later version published by -the Free Software Foundation. If the Library does not specify a -license version number, you may choose any version ever published by -the Free Software Foundation. - -**14.** If you wish to incorporate parts of the Library into other free -programs whose distribution conditions are incompatible with these, -write to the author to ask for permission. For software which is -copyrighted by the Free Software Foundation, write to the Free -Software Foundation; we sometimes make exceptions for this. Our -decision will be guided by the two goals of preserving the free status -of all derivatives of our free software and of promoting the sharing -and reuse of software generally. - -### NO WARRANTY - -**15.** BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO -WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. -EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR -OTHER PARTIES PROVIDE THE LIBRARY “AS IS” WITHOUT WARRANTY OF ANY -KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE -LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME -THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - -**16.** IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN -WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY -AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU -FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR -CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE -LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING -RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A -FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF -SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH -DAMAGES. - -END OF TERMS AND CONDITIONS - -### How to Apply These Terms to Your New Libraries - -If you develop a new library, and you want it to be of the greatest -possible use to the public, we recommend making it free software that -everyone can redistribute and change. You can do so by permitting -redistribution under these terms (or, alternatively, under the terms of the -ordinary General Public License). - -To apply these terms, attach the following notices to the library. It is -safest to attach them to the start of each source file to most effectively -convey the exclusion of warranty; and each file should have at least the -“copyright” line and a pointer to where the full notice is found. - - - Copyright (C) - - 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 - -Also add information on how to contact you by electronic and paper mail. - -You should also get your employer (if you work as a programmer) or your -school, if any, to sign a “copyright disclaimer” for the library, if -necessary. Here is a sample; alter the names: - - Yoyodyne, Inc., hereby disclaims all copyright interest in the - library `Frob' (a library for tweaking knobs) written by James Random Hacker. - - , 1 April 1990 - Ty Coon, President of Vice - -That's all there is to it! - -GNU Free Documentation License -============================== - -Version 1.3, 3 November 2008 - -Copyright © 2000, 2001, 2002, 2007, 2008 Free Software Foundation, Inc. <> - -Everyone is permitted to copy and distribute verbatim copies -of this license document, but changing it is not allowed. - -### 0. PREAMBLE - -The purpose of this License is to make a manual, textbook, or other -functional and useful document “free” in the sense of freedom: to -assure everyone the effective freedom to copy and redistribute it, -with or without modifying it, either commercially or noncommercially. -Secondarily, this License preserves for the author and publisher a way -to get credit for their work, while not being considered responsible -for modifications made by others. - -This License is a kind of “copyleft”, which means that derivative -works of the document must themselves be free in the same sense. It -complements the GNU General Public License, which is a copyleft -license designed for free software. - -We have designed this License in order to use it for manuals for free -software, because free software needs free documentation: a free -program should come with manuals providing the same freedoms that the -software does. But this License is not limited to software manuals; -it can be used for any textual work, regardless of subject matter or -whether it is published as a printed book. We recommend this License -principally for works whose purpose is instruction or reference. - - -### 1. APPLICABILITY AND DEFINITIONS - -This License applies to any manual or other work, in any medium, that -contains a notice placed by the copyright holder saying it can be -distributed under the terms of this License. Such a notice grants a -world-wide, royalty-free license, unlimited in duration, to use that -work under the conditions stated herein. The “Document”, below, -refers to any such manual or work. Any member of the public is a -licensee, and is addressed as “you”. You accept the license if you -copy, modify or distribute the work in a way requiring permission -under copyright law. - -A “Modified Version” of the Document means any work containing the -Document or a portion of it, either copied verbatim, or with -modifications and/or translated into another language. - -A “Secondary Section” is a named appendix or a front-matter section of -the Document that deals exclusively with the relationship of the -publishers or authors of the Document to the Document's overall -subject (or to related matters) and contains nothing that could fall -directly within that overall subject. (Thus, if the Document is in -part a textbook of mathematics, a Secondary Section may not explain -any mathematics.) The relationship could be a matter of historical -connection with the subject or with related matters, or of legal, -commercial, philosophical, ethical or political position regarding -them. - -The “Invariant Sections” are certain Secondary Sections whose titles -are designated, as being those of Invariant Sections, in the notice -that says that the Document is released under this License. If a -section does not fit the above definition of Secondary then it is not -allowed to be designated as Invariant. The Document may contain zero -Invariant Sections. If the Document does not identify any Invariant -Sections then there are none. - -The “Cover Texts” are certain short passages of text that are listed, -as Front-Cover Texts or Back-Cover Texts, in the notice that says that -the Document is released under this License. A Front-Cover Text may -be at most 5 words, and a Back-Cover Text may be at most 25 words. - -A “Transparent” copy of the Document means a machine-readable copy, -represented in a format whose specification is available to the -general public, that is suitable for revising the document -straightforwardly with generic text editors or (for images composed of -pixels) generic paint programs or (for drawings) some widely available -drawing editor, and that is suitable for input to text formatters or -for automatic translation to a variety of formats suitable for input -to text formatters. A copy made in an otherwise Transparent file -format whose markup, or absence of markup, has been arranged to thwart -or discourage subsequent modification by readers is not Transparent. -An image format is not Transparent if used for any substantial amount -of text. A copy that is not “Transparent” is called “Opaque”. - -Examples of suitable formats for Transparent copies include plain -ASCII without markup, Texinfo input format, LaTeX input format, SGML -or XML using a publicly available DTD, and standard-conforming simple -HTML, PostScript or PDF designed for human modification. Examples of -transparent image formats include PNG, XCF and JPG. Opaque formats -include proprietary formats that can be read and edited only by -proprietary word processors, SGML or XML for which the DTD and/or -processing tools are not generally available, and the -machine-generated HTML, PostScript or PDF produced by some word -processors for output purposes only. - -The “Title Page” means, for a printed book, the title page itself, -plus such following pages as are needed to hold, legibly, the material -this License requires to appear in the title page. For works in -formats which do not have any title page as such, “Title Page” means -the text near the most prominent appearance of the work's title, -preceding the beginning of the body of the text. - -The “publisher” means any person or entity that distributes copies of -the Document to the public. - -A section “Entitled XYZ” means a named subunit of the Document whose -title either is precisely XYZ or contains XYZ in parentheses following -text that translates XYZ in another language. (Here XYZ stands for a -specific section name mentioned below, such as “Acknowledgements”, -“Dedications”, “Endorsements”, or “History”.) To “Preserve the Title” -of such a section when you modify the Document means that it remains a -section “Entitled XYZ” according to this definition. - -The Document may include Warranty Disclaimers next to the notice which -states that this License applies to the Document. These Warranty -Disclaimers are considered to be included by reference in this -License, but only as regards disclaiming warranties: any other -implication that these Warranty Disclaimers may have is void and has -no effect on the meaning of this License. - -### 2. VERBATIM COPYING - -You may copy and distribute the Document in any medium, either -commercially or noncommercially, provided that this License, the -copyright notices, and the license notice saying this License applies -to the Document are reproduced in all copies, and that you add no -other conditions whatsoever to those of this License. You may not use -technical measures to obstruct or control the reading or further -copying of the copies you make or distribute. However, you may accept -compensation in exchange for copies. If you distribute a large enough -number of copies you must also follow the conditions in section 3. - -You may also lend copies, under the same conditions stated above, and -you may publicly display copies. - - -### 3. COPYING IN QUANTITY - -If you publish printed copies (or copies in media that commonly have -printed covers) of the Document, numbering more than 100, and the -Document's license notice requires Cover Texts, you must enclose the -copies in covers that carry, clearly and legibly, all these Cover -Texts: Front-Cover Texts on the front cover, and Back-Cover Texts on -the back cover. Both covers must also clearly and legibly identify -you as the publisher of these copies. The front cover must present -the full title with all words of the title equally prominent and -visible. You may add other material on the covers in addition. -Copying with changes limited to the covers, as long as they preserve -the title of the Document and satisfy these conditions, can be treated -as verbatim copying in other respects. - -If the required texts for either cover are too voluminous to fit -legibly, you should put the first ones listed (as many as fit -reasonably) on the actual cover, and continue the rest onto adjacent -pages. - -If you publish or distribute Opaque copies of the Document numbering -more than 100, you must either include a machine-readable Transparent -copy along with each Opaque copy, or state in or with each Opaque copy -a computer-network location from which the general network-using -public has access to download using public-standard network protocols -a complete Transparent copy of the Document, free of added material. -If you use the latter option, you must take reasonably prudent steps, -when you begin distribution of Opaque copies in quantity, to ensure -that this Transparent copy will remain thus accessible at the stated -location until at least one year after the last time you distribute an -Opaque copy (directly or through your agents or retailers) of that -edition to the public. - -It is requested, but not required, that you contact the authors of the -Document well before redistributing any large number of copies, to -give them a chance to provide you with an updated version of the -Document. - - -### 4. MODIFICATIONS - -You may copy and distribute a Modified Version of the Document under -the conditions of sections 2 and 3 above, provided that you release -the Modified Version under precisely this License, with the Modified -Version filling the role of the Document, thus licensing distribution -and modification of the Modified Version to whoever possesses a copy -of it. In addition, you must do these things in the Modified Version: - -* **A.** Use in the Title Page (and on the covers, if any) a title distinct -from that of the Document, and from those of previous versions -(which should, if there were any, be listed in the History section -of the Document). You may use the same title as a previous version -if the original publisher of that version gives permission. -* **B.** List on the Title Page, as authors, one or more persons or entities -responsible for authorship of the modifications in the Modified -Version, together with at least five of the principal authors of the -Document (all of its principal authors, if it has fewer than five), -unless they release you from this requirement. -* **C.** State on the Title page the name of the publisher of the -Modified Version, as the publisher. -* **D.** Preserve all the copyright notices of the Document. -* **E.** Add an appropriate copyright notice for your modifications -adjacent to the other copyright notices. -* **F.** Include, immediately after the copyright notices, a license notice -giving the public permission to use the Modified Version under the -terms of this License, in the form shown in the Addendum below. -* **G.** Preserve in that license notice the full lists of Invariant Sections -and required Cover Texts given in the Document's license notice. -* **H.** Include an unaltered copy of this License. -* **I.** Preserve the section Entitled “History”, Preserve its Title, and add -to it an item stating at least the title, year, new authors, and -publisher of the Modified Version as given on the Title Page. If -there is no section Entitled “History” in the Document, create one -stating the title, year, authors, and publisher of the Document as -given on its Title Page, then add an item describing the Modified -Version as stated in the previous sentence. -* **J.** Preserve the network location, if any, given in the Document for -public access to a Transparent copy of the Document, and likewise -the network locations given in the Document for previous versions -it was based on. These may be placed in the “History” section. -You may omit a network location for a work that was published at -least four years before the Document itself, or if the original -publisher of the version it refers to gives permission. -* **K.** For any section Entitled “Acknowledgements” or “Dedications”, -Preserve the Title of the section, and preserve in the section all -the substance and tone of each of the contributor acknowledgements -and/or dedications given therein. -* **L.** Preserve all the Invariant Sections of the Document, -unaltered in their text and in their titles. Section numbers -or the equivalent are not considered part of the section titles. -* **M.** Delete any section Entitled “Endorsements”. Such a section -may not be included in the Modified Version. -* **N.** Do not retitle any existing section to be Entitled “Endorsements” -or to conflict in title with any Invariant Section. -* **O.** Preserve any Warranty Disclaimers. - -If the Modified Version includes new front-matter sections or -appendices that qualify as Secondary Sections and contain no material -copied from the Document, you may at your option designate some or all -of these sections as invariant. To do this, add their titles to the -list of Invariant Sections in the Modified Version's license notice. -These titles must be distinct from any other section titles. - -You may add a section Entitled “Endorsements”, provided it contains -nothing but endorsements of your Modified Version by various -parties--for example, statements of peer review or that the text has -been approved by an organization as the authoritative definition of a -standard. - -You may add a passage of up to five words as a Front-Cover Text, and a -passage of up to 25 words as a Back-Cover Text, to the end of the list -of Cover Texts in the Modified Version. Only one passage of -Front-Cover Text and one of Back-Cover Text may be added by (or -through arrangements made by) any one entity. If the Document already -includes a cover text for the same cover, previously added by you or -by arrangement made by the same entity you are acting on behalf of, -you may not add another; but you may replace the old one, on explicit -permission from the previous publisher that added the old one. - -The author(s) and publisher(s) of the Document do not by this License -give permission to use their names for publicity for or to assert or -imply endorsement of any Modified Version. - - -### 5. COMBINING DOCUMENTS - -You may combine the Document with other documents released under this -License, under the terms defined in section 4 above for modified -versions, provided that you include in the combination all of the -Invariant Sections of all of the original documents, unmodified, and -list them all as Invariant Sections of your combined work in its -license notice, and that you preserve all their Warranty Disclaimers. - -The combined work need only contain one copy of this License, and -multiple identical Invariant Sections may be replaced with a single -copy. If there are multiple Invariant Sections with the same name but -different contents, make the title of each such section unique by -adding at the end of it, in parentheses, the name of the original -author or publisher of that section if known, or else a unique number. -Make the same adjustment to the section titles in the list of -Invariant Sections in the license notice of the combined work. - -In the combination, you must combine any sections Entitled “History” -in the various original documents, forming one section Entitled -“History”; likewise combine any sections Entitled “Acknowledgements”, -and any sections Entitled “Dedications”. You must delete all sections -Entitled “Endorsements”. - - -### 6. COLLECTIONS OF DOCUMENTS - -You may make a collection consisting of the Document and other -documents released under this License, and replace the individual -copies of this License in the various documents with a single copy -that is included in the collection, provided that you follow the rules -of this License for verbatim copying of each of the documents in all -other respects. - -You may extract a single document from such a collection, and -distribute it individually under this License, provided you insert a -copy of this License into the extracted document, and follow this -License in all other respects regarding verbatim copying of that -document. - - -### 7. AGGREGATION WITH INDEPENDENT WORKS - -A compilation of the Document or its derivatives with other separate -and independent documents or works, in or on a volume of a storage or -distribution medium, is called an “aggregate” if the copyright -resulting from the compilation is not used to limit the legal rights -of the compilation's users beyond what the individual works permit. -When the Document is included in an aggregate, this License does not -apply to the other works in the aggregate which are not themselves -derivative works of the Document. - -If the Cover Text requirement of section 3 is applicable to these -copies of the Document, then if the Document is less than one half of -the entire aggregate, the Document's Cover Texts may be placed on -covers that bracket the Document within the aggregate, or the -electronic equivalent of covers if the Document is in electronic form. -Otherwise they must appear on printed covers that bracket the whole -aggregate. - - -### 8. TRANSLATION - -Translation is considered a kind of modification, so you may -distribute translations of the Document under the terms of section 4. -Replacing Invariant Sections with translations requires special -permission from their copyright holders, but you may include -translations of some or all Invariant Sections in addition to the -original versions of these Invariant Sections. You may include a -translation of this License, and all the license notices in the -Document, and any Warranty Disclaimers, provided that you also include -the original English version of this License and the original versions -of those notices and disclaimers. In case of a disagreement between -the translation and the original version of this License or a notice -or disclaimer, the original version will prevail. - -If a section in the Document is Entitled “Acknowledgements”, -“Dedications”, or “History”, the requirement (section 4) to Preserve -its Title (section 1) will typically require changing the actual -title. - - -### 9. TERMINATION - -You may not copy, modify, sublicense, or distribute the Document -except as expressly provided under this License. Any attempt -otherwise to copy, modify, sublicense, or distribute it is void, and -will automatically terminate your rights under this License. - -However, if you cease all violation of this License, then your license -from a particular copyright holder is reinstated (a) provisionally, -unless and until the copyright holder explicitly and finally -terminates your license, and (b) permanently, if the copyright holder -fails to notify you of the violation by some reasonable means prior to -60 days after the cessation. - -Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - -Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, receipt of a copy of some or all of the same material does -not give you any rights to use it. - - -### 10. FUTURE REVISIONS OF THIS LICENSE - -The Free Software Foundation may publish new, revised versions of the -GNU Free Documentation License from time to time. Such new versions -will be similar in spirit to the present version, but may differ in -detail to address new problems or concerns. See -<>. - -Each version of the License is given a distinguishing version number. -If the Document specifies that a particular numbered version of this -License “or any later version” applies to it, you have the option of -following the terms and conditions either of that specified version or -of any later version that has been published (not as a draft) by the -Free Software Foundation. If the Document does not specify a version -number of this License, you may choose any version ever published (not -as a draft) by the Free Software Foundation. If the Document -specifies that a proxy can decide which future versions of this -License can be used, that proxy's public statement of acceptance of a -version permanently authorizes you to choose that version for the -Document. - -### 11. RELICENSING - -“Massive Multiauthor Collaboration Site” (or “MMC Site”) means any -World Wide Web server that publishes copyrightable works and also -provides prominent facilities for anybody to edit those works. A -public wiki that anybody can edit is an example of such a server. A -“Massive Multiauthor Collaboration” (or “MMC”) contained in the site -means any set of copyrightable works thus published on the MMC site. - -“CC-BY-SA” means the Creative Commons Attribution-Share Alike 3.0 -license published by Creative Commons Corporation, a not-for-profit -corporation with a principal place of business in San Francisco, -California, as well as future copyleft versions of that license -published by that same organization. - -“Incorporate” means to publish or republish a Document, in whole or in -part, as part of another Document. - -An MMC is “eligible for relicensing” if it is licensed under this -License, and if all works that were first published under this License -somewhere other than this MMC, and subsequently incorporated in whole or -in part into the MMC, (1) had no cover texts or invariant sections, and -(2) were thus incorporated prior to November 1, 2008. - -The operator of an MMC Site may republish an MMC contained in the site -under CC-BY-SA on the same site at any time before August 1, 2009, -provided the MMC is eligible for relicensing. - - -## ADDENDUM: How to use this License for your documents - -To use this License in a document you have written, include a copy of -the License in the document and put the following copyright and -license notices just after the title page: - - Copyright (c) YEAR YOUR NAME. - Permission is granted to copy, distribute and/or modify this document - under the terms of the GNU Free Documentation License, Version 1.3 - or any later version published by the Free Software Foundation; - with no Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts. - A copy of the license is included in the section entitled “GNU - Free Documentation License”. +The library itself is distributed under the GNU Lesser General Public +License v2.1 or later — see [`COPYING.LESSER`](COPYING.LESSER) for the +full text. + +[Back to TOC](#table-of-contents) -If you have Invariant Sections, Front-Cover Texts and Back-Cover Texts, -replace the `with...Texts.` line with this: +## Thanks - with the Invariant Sections being LIST THEIR TITLES, with the - Front-Cover Texts being LIST, and with the Back-Cover Texts being LIST. +libhttpserver builds on the work of the GNU libmicrohttpd developers and +the many contributors to libhttpserver itself (see the project's git +history for the complete list). Particular thanks to everyone who has +filed issues, sent patches, and stress-tested the v2.0 redesign. -If you have Invariant Sections without Cover Texts, or some other -combination of the three, merge those two alternatives to suit the -situation. +If libhttpserver is useful to you, consider sponsoring continued +maintenance: . -If your document contains nontrivial examples of program code, we -recommend releasing these examples in parallel under your choice of -free software license, such as the GNU General Public License, -to permit their use in free software. +[Back to TOC](#table-of-contents) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md new file mode 100644 index 000000000..5b8e1f2fc --- /dev/null +++ b/RELEASE_NOTES.md @@ -0,0 +1,438 @@ +# libhttpserver v2.0 — Release Notes + +> **Status:** Informational, not a compatibility commitment. +> This document summarises every public-surface change in v2.0 to help v1 +> consumers port. It is not part of the API contract and is not exhaustive +> — when in doubt, the headers in `src/httpserver/` are authoritative. + +## TL;DR + +libhttpserver v2.0 is a **clean cutover** from the v1.x line: no shims, no +deprecated overloads, no inline namespace. Handlers are now lambdas +(`ws.on_get("/", [](auto&){ return http_response::string("hi"); })`); +responses are value types built from `http_response::string` / `file` / +`iovec` / `pipe` / `empty` / `deferred` / `unauthorized` factories; +request getters return `const&` / `string_view` and never insert on miss; +the threading and error-propagation contracts are documented (DR-008, +DR-009); features disabled at build time report at runtime via +`feature_unavailable` or a documented sentinel value instead of vanishing +from the API. SOVERSION bumps from 1 to 2 and v2 ships as a +`libhttpserver2` binary that is parallel-installable with `libhttpserver1`. +v1.x is end-of-life on the day v2.0 ships. + +## What's gone + +- **Response subclasses.** `string_response`, `file_response`, + `iovec_response`, `pipe_response`, `deferred_response`, `empty_response`, + `basic_auth_fail_response`, `digest_auth_fail_response` are removed. + Build responses through the `http_response::*` factory chain instead + (see "What's renamed" for the 1:1 mapping). +- **Raw-pointer registration.** `register_resource(string, http_resource*, bool family)` + is gone. Use `register_path(unique_ptr|shared_ptr)` or + `register_prefix(unique_ptr|shared_ptr)`. + `register_ws_resource(string, websocket_handler*)` is gone. Use the + smart-pointer overload. +- **`sweet_kill`.** Removed. Use `stop_and_wait()` (or `stop()` for the + signal-only form). +- **IP allow/deny verbs.** The v1 `ban_ip` / `unban_ip` / `allow_ip` / + `disallow_ip` quartet is renamed for a symmetric, unambiguous surface: + `deny_ip` / `remove_denied_ip` operate the deny list, and `allow_ip` / + `remove_allowed_ip` operate the allow list. `default_policy(ACCEPT)` + makes the deny list the exception list (block only these); `REJECT` + makes the allow list the exception list (permit only these). An allow + entry overrides a matching deny entry. See "What's renamed". +- **Paired `no_*` boolean setters.** `no_basic_auth`, `no_digest_auth`, + `no_ssl`, `no_debug`, `no_pedantic`, `no_deferred`, `no_regex_checking`, + `no_ban_system`, `no_post_process`, `no_single_resource`, `no_ipv6`, + `no_dual_stack`, `no_alpn`, `no_thread_safety`, `no_listen_socket`, + `no_put_processed_data_to_content`, and + `no_generate_random_filename_on_upload` are removed (the full v1 + `no_*` family). The remaining single setter takes a `bool`: + e.g. `cw.ssl(false)` replaces `cw.no_ssl()`. +- **`#define` constants in public headers.** `DEFAULT_WS_PORT`, + `DEFAULT_WS_TIMEOUT`, and the `*_ERROR` / `*_RESPONSE` macros are gone. + Their `constexpr` replacements live in the `httpserver::constants` + namespace. +- **`gnutls_session_t`-returning methods on `http_request`.** Removed. + Use the high-level accessors: `get_client_cert_dn()`, + `get_client_cert_issuer_dn()`, `get_client_cert_fingerprint()`, + `get_cipher_suite()`, `get_protocol_version()`, `get_client_kx()`, + `get_client_cert_type()`, `get_client_credential_type()`. +- **Public virtuals on `http_response`.** `get_raw_response`, + `decorate_response`, `enqueue_response` are gone. `http_response` is no + longer an open base class; it is a value type with no virtual methods. +- **`#ifdef HAVE_BAUTH` / `HAVE_DAUTH` / `HAVE_GNUTLS` / `HAVE_WEBSOCKET` + guards in public headers.** Removed. The public API is now + build-flag-invariant: every method exists on every build. Calls into a + feature disabled at configure time throw `feature_unavailable` or + return a documented sentinel; query the runtime configuration via + `ws.features()`. +- **Public transitive includes of backend C headers.** ``, + ``, ``, `` no longer leak + through ``. A consumer that needs MHD or GnuTLS types + must include them directly. +- **The implicit conversion** `webserver ws = cw;` (where `cw` is a + `create_webserver`). The constructor is now `explicit`. +- **v1 compat auth-handler shim.** `httpserver::compat::auth_handler_v1_ptr`, + `httpserver::compat::adapt_legacy_auth`, and the + `[[deprecated]] create_webserver::auth_handler(compat::auth_handler_v1_ptr)` + overload are removed (TASK-067). The transitional shim that let v1 + callers spell their auth handler as + `std::function(const http_request&)>` + was scheduled for removal "in the next release"; this is that removal. + Migrate to the canonical `auth_handler_ptr` returning + `std::optional` (`std::nullopt` to allow, engaged value + to reject). The `namespace httpserver::compat` is dissolved with no + surviving members. **Source incompatibility:** TUs that called + `auth_handler()` fail to compile against v2.1 headers; + the legacy callable is no longer implicitly convertible to + `auth_handler_ptr`. There is no runtime ABI surface to break — the + overload was a header-only inline forwarder. +- **`create_webserver::validator(...)` dispatch semantics — confirmed + never wired (TASK-078).** The `validator` builder method and the + `validator_ptr` callback typedef on `create_webserver` are retained + unchanged — the callback has not been invoked from the dispatch path + since commit `9163a4f` (Jan 2013, "Eliminated unescaper and validator + delegates"). v2 ships the surface for source compatibility, but a + configured validator callback is dead code. The + v2 replacement is `webserver::add_hook(hook_phase::request_received, + ...)` returning `hook_action::respond_with(http_response)` to reject + a request, or `webserver::deny_ip`/`remove_denied_ip` for a connection-scoped + veto; `hook_phase::accept_decision` is observation-only (Short-circuit: + no, per `specs/architecture/04-components/hooks.md`) and cannot reject + a connection. The legacy + `validator_builder` integ test in `test/integ/basic.cpp` exercised + no validation behaviour (it only asserted the server booted with a + validator set) and has been removed; the equivalent compile-time + builder pin survives at + `test/unit/create_webserver_test.cpp::builder_validator`. +- **v1 `registered_resources*` registration maps (internal).** No + public-API change. Dispatch already routed through the v2 3-tier + route table (`lookup_v2()`) after TASK-053; the residual + registration-time maps (`registered_resources`, + `registered_resources_str`, `registered_resources_regex`) and their + shared mutex are now deleted (TASK-067). Lambda/class path-conflict + detection consults the v2 route table directly via + `find_v2_entry_by_path_`; duplicate-registration detection moved + inside `register_v2_route`; the WebSocket handler registry gains a + dedicated `registered_ws_handlers_mutex_`. Callers observe no + behavioural change. + +## What's new + +- **Lambda registration.** `ws.on_get(path, handler)`, `on_post`, + `on_put`, `on_delete`, `on_patch`, `on_options`, `on_head`. The handler + is `http_response(const http_request&)`. No subclass required. +- **Runtime-method registration.** `ws.route(http_method | method_set, + path, handler)` registers a single handler for a method known only at + runtime, or atomically for a bitmask of methods. +- **Smart-pointer resource registration.** `register_path(path, h)` and + `register_prefix(path, h)` accept `std::unique_ptr` or + `std::shared_ptr`. The boolean `family` flag that used + to live as the third positional argument of `register_resource` is gone + — the choice is encoded in the method name. +- **`http_response` factory chain.** `http_response::string(body)`, + `http_response::file(path)`, `http_response::iovec(entries)`, + `http_response::pipe(fd)`, `http_response::empty(status)`, + `http_response::deferred(...)`, + `http_response::unauthorized(scheme, realm, ...)`. Each returns a + value-type `http_response`. Chain fluent mutators: + `.with_status(int)`, `.with_header(name, value)`, `.with_footer(...)`, + `.with_cookie(...)`. +- **`feature_unavailable`.** Public `std::runtime_error` subclass thrown + by API entry points whose backend feature was disabled at configure + time (e.g. calling `ssl_cert(...)` on a `HAVE_GNUTLS=no` build). +- **`webserver::features()`.** Returns a struct of `bool`s describing + which optional features are linked in (TLS, basic auth, digest auth, + websocket, …). Use this for runtime branching instead of `#ifdef HAVE_*`. +- **`iovec_entry`.** Public POD struct (`{void* iov_base; size_t iov_len;}`) + used by `http_response::iovec`. Replaces the opaque scatter-gather + type that v1 leaked from ``. +- **`http_method` / `method_set`.** Strongly-typed enum and bitmask for + HTTP methods. `route()` accepts either; `http_resource::allow_methods` + uses `method_set`. +- **`httpserver::constants` namespace.** `constexpr` replacements for + every removed `#define`. +- **Lifecycle hook bus (`webserver::add_hook` / `http_resource::add_hook`).** + Eleven phases (`hook_phase` enum: `connection_opened`, + `accept_decision`, `request_received`, `body_chunk`, + `route_resolved`, `before_handler`, `handler_exception`, + `after_handler`, `response_sent`, `request_completed`, + `connection_closed`) spanning connection, request, routing, handler, + and response. Five of the phases are short-circuit-capable: their + handlers return `hook_action` (either `hook_action::pass()` to + continue normal processing or `hook_action::respond(http_response)` to + short-circuit with a custom response). Multi-subscriber, server-wide + and per-route. The v1 single-slot setters (`log_access`, + `not_found_handler`, `method_not_allowed_handler`, + `internal_error_handler`, `auth_handler`) survive as documented aliases + for the corresponding `add_hook` calls. Each registration returns a + move-only `hook_handle` whose destructor erases the entry; + `hook_handle::detach()` keeps the registration alive for the + webserver's lifetime. See `specs/architecture/04-components/hooks.md` + and [`README.md#lifecycle-hooks`](README.md#lifecycle-hooks). Closes: + #332 (denied-IP log, `examples/denied_ip_log.cpp`), #281 + #69 + (CLF / time-taken access log, `examples/clf_access_log.cpp`), #273 + (early 413, `examples/early_413.cpp`); partially closes #272 + (per-chunk observation; body steal deferred to v2.1). + +## What's renamed (v1 → v2) + +Every entry below is a single line so a v1 user can grep for the v1 name +and see the v2 replacement. + +| v1 | v2 | +|---|---| +| `sweet_kill` | `stop_and_wait` | +| `ban_ip` | `deny_ip` | +| `unban_ip` | `remove_denied_ip` | +| `allow_ip` | `allow_ip` (unchanged; allow list) | +| `disallow_ip` | `remove_allowed_ip` | +| `not_found_resource` (setter) | `not_found_handler` | +| `method_not_allowed_resource` (setter) | `method_not_allowed_handler` | +| `internal_error_resource` (setter) | `internal_error_handler` | +| `render_GET` | `render_get` | +| `render_POST` | `render_post` | +| `render_PUT` | `render_put` | +| `render_DELETE` | `render_delete` | +| `render_HEAD` | `render_head` | +| `render_OPTIONS` | `render_options` | +| `render_PATCH` | `render_patch` | +| `render_CONNECT` | `render_connect` | +| `render_TRACE` | `render_trace` | +| `webserver(create_webserver const&)` — implicit | `explicit webserver(create_webserver const&)` | +| `register_resource(string, http_resource*, bool family)` — raw pointer + bool flag | `register_path` / `register_prefix` taking `unique_ptr` or `shared_ptr` | +| `register_ws_resource(string, websocket_handler*)` — raw pointer | `register_ws_resource(string, unique_ptr)` (and a `shared_ptr` overload) | +| `string_response` | `http_response::string` | +| `file_response` | `http_response::file` | +| `iovec_response` | `http_response::iovec` | +| `pipe_response` | `http_response::pipe` | +| `empty_response` | `http_response::empty` | +| `deferred_response` | `http_response::deferred` | +| `basic_auth_fail_response` | `http_response::unauthorized` | +| `digest_auth_fail_response` | `http_response::unauthorized` | +| `auth_handler_ptr` returning `std::shared_ptr` | `auth_handler_ptr` returning `std::optional` | + +> **Note:** `shoutCAST()` is the sole camelCase survivor in the v2 public API. +> The name maps to the SHOUTcast streaming protocol and is intentionally +> preserved unchanged (PRD §3.7). + +## What changed semantically + +- **Handlers return `http_response` by value.** v1 returned + `std::unique_ptr` (and earlier `std::shared_ptr`); + v2 returns a value. The framework moves it into the dispatch path. No + heap allocation is required for small responses (small-buffer optimisation + inside `http_response`). +- **Centralized auth handler returns `std::optional`.** + The `auth_handler` callback signature is now + `std::function(const http_request&)>` + (earlier v2 work-in-progress shipped + `std::function(const http_request&)>`). + Return `std::nullopt` to allow the request; return an `http_response` + to reject. Removes one heap allocation per authenticated request. +- **`http_request` getters return `const&` / `string_view`.** v1's + `get_header(name)` (and `get_arg`, `get_cookie`, `get_footer`) returned + by value and inserted an empty entry into the request map on miss; v2's + versions return a reference / `string_view` and never mutate the + request. Container getters (`get_headers()`, `get_args()`, + `get_path_pieces()`, `get_files()`) return `const&` to the underlying + map. +- **`http_response::get_header` / `get_footer` / `get_cookie` are + const and do not insert on miss.** Mutation uses the + `with_header` / `with_footer` / `with_cookie` chain, which returns + `*this` by reference (`&` on lvalue calls, `&&` on rvalue calls) for + fluent assembly. +- **`webserver(create_webserver const&)` is `explicit`.** Direct-init + `webserver ws{cw};` rather than copy-init `webserver ws = cw;`. +- **Default internal-error response body is now a fixed string.** v1's + default 500 body included the originating exception's `e.what()` + text. v2.0 + ([DR-009 Revision 1](specs/architecture/11-decisions/DR-009.md)) + sends `"Internal Server Error"` instead, eliminating a CWE-209 + information-disclosure surface (exception messages routinely embed + file paths, SQL fragments, internal identifiers, and + attacker-influenced input). The `log_error` callback continues to + receive the verbatim message. To restore the v1 verbose-body + behaviour for development, set `.expose_exception_messages(true)` + on the builder. Configured `internal_error_handler` callbacks are + unaffected — they still receive the message and can build any body + they want. +- **`http_request::operator<<` redacts credentials by default.** + v1 (and earlier v2 builds) emitted `pass:""`, + `Authorization`/`Proxy-Authorization` header values, and cookie values + verbatim into diagnostic output, leaking every credential into any + log aggregation pipeline that captures operator-stream dumps + (CWE-312, CWE-532, OWASP A09:2021). Digest credentials are protected + through the same Authorization-header path (there was never a separate + `digested_pass` emit field). v2.0 replaces those fields with the fixed + token `` in the default stream output. To restore the v1 + verbose form for local development, call `.expose_credentials_in_logs(true)` + on the `create_webserver` builder — this flag is intended for development + only and must not be set in production deployments. +- **`http_response::pipe` no longer accepts a `size_hint`.** Earlier v2 + work-in-progress builds exposed `pipe(int fd, std::size_t size_hint = 0)` + with `size_hint` reserved for future use; the parameter was never + consulted on the dispatch path. v2.0 ships the minimal `pipe(int fd)` + signature (TASK-063, PRD §3.5 / `PRD-RSP-REQ-001`). Source callers + passing a second argument must drop it; no behavioural change. There + is no honoring path planned — `Content-Length` synthesis would lie + when the pipe yields a different byte count, and libmicrohttpd's + `MHD_create_response_from_pipe` takes no size. +- **Structured cookie type (TASK-064).** The string-blob cookie API + on `http_response` is now `[[deprecated]]` in favour of a typed + `httpserver::cookie` value (new public header ``). + Construct with fluent setters — `cookie{}.with_name(...).with_value(...) + .with_domain(...).with_path(...).with_expires(epoch_seconds).with_max_age(s) + .with_secure(true).with_http_only(true).with_same_site(same_site_mode::strict)` + — then hand to `http_response::with_cookie(cookie)`. The dispatch path + emits one RFC 6265 §4.1 well-formed `Set-Cookie` header per entry with + a fixed attribute order (`name=value; Expires; Max-Age; Domain; Path; + Secure; HttpOnly; SameSite`); `SameSite=None` auto-coerces `Secure` on + the wire. The matching request-side accessor is `http_request:: + get_cookies_parsed()`, returning `const std::vector&` + backed by a per-request lazy cache. Legacy `with_cookie(std::string, + std::string)`, `get_cookie(...)`, and `get_cookies()` still compile but + emit `[[deprecated]]`; they will be removed in v2.1. The new APIs reject + CR/LF/NUL plus `;` in values (attribute-injection guard, CWE-113); the + pre-TASK-064 wire footgun of `with_cookie("name", "v; Path=/admin")` + silently emitting attributes is gone. + +## Threading + +The threading contract is now documented; in v1 it was implicit. +See [README.md "Threading contract"](README.md), architecture §5.1 +([specs/architecture/05-cross-cutting.md](specs/architecture/05-cross-cutting.md)) +and DR-008 ([specs/architecture/11-decisions/DR-008.md](specs/architecture/11-decisions/DR-008.md)) +for the full statement. The load-bearing points for porters: + +- Handler invocations from MHD worker threads run concurrently. Shared + state owned by an `http_resource` subclass (or captured by a lambda) + must be synchronised by the application. +- `start()` is one-shot; **do not** call `stop()` or `stop_and_wait()` + from inside a handler thread — MHD's joinable internal thread cannot + join itself and the call aborts the process (DR-008). +- `~webserver()` runs `stop_and_wait()` if the server is still running, + so the same self-join restriction applies to destruction from a + handler thread. + +## Error propagation + +The error-propagation contract is now documented; in v1 it was implicit. +See [README.md "Error propagation"](README.md), architecture §5.2 +([specs/architecture/05-cross-cutting.md](specs/architecture/05-cross-cutting.md)) +and DR-009 ([specs/architecture/11-decisions/DR-009.md](specs/architecture/11-decisions/DR-009.md)) +for the full statement. The load-bearing points for porters: + +- A handler that throws lands at the configured + `internal_error_handler` (a `std::function`). + The `string_view` carries `what()` from the caught exception. Default + behaviour: log and return `500`. When no `internal_error_handler` is + configured, the default response body is the fixed string + `"Internal Server Error"` (DR-009 Revision 1, CWE-209 fix); set + `.expose_exception_messages(true)` on the builder to restore the v1 + message-in-body behaviour for development. +- Calling an API entry point whose backend feature is disabled at + configure time (e.g. `ssl_cert(...)` on a `HAVE_GNUTLS=no` build) + throws `feature_unavailable` — a public `std::runtime_error` subclass. + Build-flag-disabled features therefore surface at **runtime** rather + than disappearing from the API at compile time; query + `ws.features()` to branch defensively. +- The framework never swallows exceptions silently. Anything that + escapes a handler is logged via the configured `log_error` callback + before the `internal_error_handler` fires. + +## Build prerequisites + +- **C++20 floor.** libhttpserver 1.x's last release was C++17-compatible; + v2.0 drops C++17. Concepts, ``, ``, and `consteval` appear + in public headers. +- **Toolchains known good out of the box:** Debian 13 GCC 14.2, + RHEL 10 GCC 14, FreeBSD 14 Clang 18+, current Apple Clang, and + Homebrew GCC 15+. +- **RHEL 9.** Stock GCC 11 on RHEL 9 is too old. Install Red Hat's + `gcc-toolset-14` overlay and `source scl_source enable gcc-toolset-14` + before configuring. + +See [specs/architecture/08-build-and-packaging.md](specs/architecture/08-build-and-packaging.md) +for the full matrix. + +## SOVERSION and packaging + +SOVERSION bumps from `1` to `2`. v2 ships as `libhttpserver2` (binary) +with `libhttpserver2-dev` / `libhttpserver2-devel` for headers and +pkg-config. v1's `libhttpserver1` and v2's `libhttpserver2` are +**parallel-installable**: applications mid-port can link against both +side by side, including under the same `/usr` prefix. + +There is **no inline namespace** and **no symbol-versioning script**: +v2 is a clean rename, not an ABI overlay over v1. v1.x is end-of-life +on the day v2.0 ships — there is no v1 maintenance branch (PRD §1, +DR-011 ([specs/architecture/11-decisions/DR-011.md](specs/architecture/11-decisions/DR-011.md))). + +## Test infrastructure + +- **`LT_SKIP` / `LT_SKIP_IF` macros (TASK-076).** The internal + `test/littletest.hpp` framework now distinguishes SKIP from PASS. + Test bodies that depend on a runtime resource (for example + `gnutls-cli` for the PSK connection tests in + `test/integ/ws_start_stop.cpp`) now call + `LT_SKIP_IF(cond, reason)` instead of recording a tautological + `LT_CHECK_EQ(1, 1)`. A binary whose only outcomes are skips exits + 77 (Automake's SKIP code from `test-driver`); a binary with mixed + skips and successes exits 0 and the runner summary reports + `-> N skipped`. The TLS lanes in `ws_start_stop.cpp` now skip — not + silently pass — when GnuTLS or `gnutls-cli` is absent, so a build + that silently lost TLS support is no longer hidden behind a green + CI signal. Two new CI lanes (`tls-no-cli`, plus a baseline canary) + assert the SKIP wiring fires (or does not) as configured. + +- **`` runtime-sentinel guard removed (TASK-081).** The + pthread leak detector in `test/unit/header_hygiene_test.cpp` (and + its companion regex slot in `Makefile.am`'s `HEADER_HYGIENE_FORBIDDEN`) + was unable to fire on any CI lane libhttpserver actually runs on. The + libhttpserver public surface uses STL container headers + (`std::string`, `std::vector`, `std::map`, ...) and both mainstream + C++ standard libraries — libc++ via + `<__thread/support/pthread.h>` and libstdc++ in thread-enabled mode + via `` — unconditionally drag `` in + from those headers. Because the public surface cannot be rewritten + to drop STL containers without a source-incompatible break, the + pthread guard is structurally unsatisfiable on every supported + configuration. The detector was deleted rather than kept as + dead code gated on STL-implementation-detection macros. The + remaining hygiene sentinels (``, ``, + ``, ``) continue to run on every CI lane. + +- **Parallel-install acceptance gate is now live in per-PR CI + (TASK-089).** The TASK-044 parallel-installability check + (`scripts/check-parallel-install.sh`, which builds `libhttpserver1` + from `master` alongside `libhttpserver2` and asserts both SONAMEd + shared libraries coexist in one DESTDIR) was previously opt-in only + (`make check-parallel-install`) and degraded to a silent pass (exit + 0) on five environment-quirk paths — so a PR that broke + parallel-installability never actually failed anything. It now runs + on a single baseline Linux gcc/libstdc++ lane in + `.github/workflows/verify-build.yml` (matrix key + `parallel-install: check`), and the five environment-quirk paths + (master ref missing, `git worktree add` failed, v1 + bootstrap/configure/make failed) emit a `SKIP` and **fail the job** + rather than passing. The escape hatch for genuine infrastructure + breakage is the environment variable + `HTTPSERVER_ALLOW_PARALLEL_INSTALL_SKIP=1`, which is intentionally + not set on the CI lane so skips stay fatal. A structural gate + (`scripts/check-parallel-install-lane.sh`, run on the `lint` lane) + guards the CI wiring against silent drift, and a unit test + (`scripts/test_check_parallel_install.sh`, run in every lane via + `make check`) pins the skip-or-fail exit-code contract. + +## See also + +- [README.md](README.md) — full v2.0 introduction and worked examples. +- [examples/](examples/) — every example is v2.0-idiomatic. +- [specs/product_specs.md](specs/product_specs.md) §3.1–§3.7 — the + authoritative requirement set behind each change. +- [specs/architecture/13-documentation.md](specs/architecture/13-documentation.md) + — architecture deliverable definition that names this file as an explicit + deliverable; the requirement lineage for this document. +- [ChangeLog](ChangeLog) — formal per-version log. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000..9a3617020 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,17 @@ +# Security Policy + +Please report suspected security vulnerabilities privately. Do not include +exploit details in a public issue or pull request before coordinated disclosure. + +Use **Report a vulnerability** on the +[GitHub Security Advisories page](https://github.com/etr/libhttpserver/security/advisories) +when it is available. Otherwise, email the maintainer at +[electrictwister2000@gmail.com](mailto:electrictwister2000@gmail.com). + +Include the affected version or commit, build configuration, dependency versions, +impact, and reproduction steps or a minimal proof of concept. Reports affecting +older releases are welcome; please identify the exact version tested. + +The maintainer and reporter will coordinate a fix, disclosure timing, and credit. +Please identify any existing CVE request so that duplicate identifiers can be +avoided, and state how you would like to be credited. diff --git a/bootstrap b/bootstrap index 67425d32d..7de7c1be9 100755 --- a/bootstrap +++ b/bootstrap @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # # This file is part of libhttpserver # Copyright (C) 2011, 2012, 2013, 2014, 2015 Sebastiano Merlino @@ -26,11 +26,3 @@ else fi automake --add-missing autoconf - -# Optionally do the build as well. -if [ "$1" = "-build" -o "$1" = "--build" ] ; then - shift - ./configure "$@" - make - make check -fi diff --git a/ci-report-coverage b/ci-report-coverage index 1b017d6c9..d8f72e164 100755 --- a/ci-report-coverage +++ b/ci-report-coverage @@ -10,5 +10,10 @@ cp -R ../test/* ./test/ cp -R ../test/integ/* ./test/integ/ cp -R ../test/unit/* ./test/unit/ -echo "Sending coveralls json report" -coveralls --gcov-options '\-lp' +echo "Sending json report" +for filename in `find . | egrep '\.cpp'`; +do + gcov -n -o . $filename > /dev/null; +done + +codecov diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 000000000..e5e69692a --- /dev/null +++ b/codecov.yml @@ -0,0 +1,32 @@ +codecov: + require_ci_to_pass: yes + +coverage: + precision: 2 + round: down + range: "70...100" + status: + project: + default: + target: auto + # Integration tests exercise timing-dependent logging/error paths, so + # unrelated lines can move between fully covered and partially covered + # between otherwise green runs. Permit that measured noise without + # weakening the 70% floor or patch coverage enforcement. + threshold: 1% + +parsers: + gcov: + branch_detection: + conditional: yes + loop: yes + method: no + macro: no + +comment: + layout: "reach,diff,flags,files,footer" + behavior: default + require_changes: no + +ignore: + - "test" diff --git a/configure.ac b/configure.ac index 0f9165449..3263062d5 100644 --- a/configure.ac +++ b/configure.ac @@ -1,6 +1,6 @@ # # This file is part of libhttpserver -# Copyright (C) 2011, 2012, 2013, 2014, 2015 Sebastiano Merlino +# Copyright (C) 2011-2021 Sebastiano Merlino # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -20,9 +20,9 @@ # Process this file with autoconf to produce a configure script. AC_PREREQ(2.57) -m4_define([libhttpserver_MAJOR_VERSION],[0])dnl -m4_define([libhttpserver_MINOR_VERSION],[13])dnl -m4_define([libhttpserver_REVISION],[0])dnl +m4_define([libhttpserver_MAJOR_VERSION],[2])dnl +m4_define([libhttpserver_MINOR_VERSION],[0])dnl +m4_define([libhttpserver_REVISION],[1])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 AC_INIT([libhttpserver], libhttpserver_PKG_VERSION, [electrictwister2000@gmail.com]) @@ -31,15 +31,26 @@ AC_CONFIG_HEADERS([config.h]) AC_CONFIG_MACRO_DIR([m4]) AC_CANONICAL_HOST +# TASK-088: sgcheck (exp-sgcheck) was removed upstream in Valgrind 3.20; the +# CI Valgrind (ubuntu-latest -> 3.22) no longer ships it, so it is disabled +# by default here rather than probed and skipped. helgrind + drd stay on +# (their default) so the Valgrind lane runs memcheck + both race detectors. +# AX_VALGRIND_DFLT calls must precede AX_VALGRIND_CHECK. +AX_VALGRIND_DFLT([sgcheck], [off]) +AX_VALGRIND_CHECK + OLD_CXXFLAGS=$CXXFLAGS LT_INIT AC_PROG_CC -AC_PROG_CXX([clang++]) +AC_PROG_CXX() AC_PROG_LN_S CXXFLAGS=$OLD_CXXFLAGS AC_LANG([C++]) AC_SYS_LARGEFILE +# Minimal feature-set required +AX_CXX_COMPILE_STDCXX([20], [noext], [mandatory]) + native_srcdir=$srcdir AC_MSG_CHECKING([whether it is possible to compile in the same directory]) @@ -51,37 +62,56 @@ AC_ARG_ENABLE([same-directory-build], AC_MSG_RESULT([$samedirectory]) if test x"$samedirectory" = x"no"; then - if test "`cd $srcdir; /bin/pwd`" = "`/bin/pwd`"; then + if test "`cd $srcdir; /bin/pwd`" = "`/bin/pwd`"; then AC_MSG_ERROR("you must configure in a separate build directory") fi fi is_windows=yes; +NETWORK_LIBS="" case "$host" in *-mingw*) NETWORK_HEADER="winsock2.h" - REGEX_LIBS="-lregex -lpthread -no-undefined" + ADDITIONAL_LIBS="-lpthread -no-undefined" + NETWORK_LIBS="-lws2_32" native_srcdir=$(cd $srcdir; pwd -W) ;; - *-cygwin*) + *-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" - REGEX_LIBS="-lregex -lpthread -no-undefined" + 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" - REGEX_LIBS="" + ADDITIONAL_LIBS="" is_windows=no ;; 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")]) AC_CHECK_HEADER([unistd.h],[],[AC_MSG_ERROR("unistd.h not found")]) AC_CHECK_HEADER([ctype.h],[],[AC_MSG_ERROR("cctype not found")]) -AC_CHECK_HEADER([regex.h],[],[AC_MSG_ERROR("regex.h not found")]) AC_CHECK_HEADER([sys/stat.h],[],[AC_MSG_ERROR("sys/stat.h not found")]) AC_CHECK_HEADER([sys/types.h],[],[AC_MSG_ERROR("sys/types.h not found")]) AC_CHECK_HEADER([$NETWORK_HEADER],[],[AC_MSG_ERROR("$NETWORK_HEADER not found")]) @@ -89,29 +119,90 @@ AC_CHECK_HEADER([signal.h],[],[AC_MSG_ERROR("signal.h not found")]) AC_CHECK_HEADER([gnutls/gnutls.h],[have_gnutls="yes"],[AC_MSG_WARN("gnutls/gnutls.h not found. TLS will be disabled"); have_gnutls="no"]) +# TASK-068: portable secure-zero primitive selection. Probe for +# explicit_bzero (glibc / musl / FreeBSD / OpenBSD / *BSD). The header +# httpserver/detail/secure_zero.hpp dispatches at compile time based on +# whether this is available. Windows takes the RtlSecureZeroMemory +# branch; macOS and other platforms without explicit_bzero take the +# portable volatile + asm-clobber fallback (which has the same security +# guarantee without the __STDC_WANT_LIB_EXT1__ include-order coupling +# that memset_s would impose on a transitively-included header). +# +# The HAVE_EXPLICIT_BZERO macro is pushed into AM_CXXFLAGS / AM_CFLAGS +# (matching the established libhttpserver pattern for HAVE_GNUTLS / +# HAVE_BAUTH / etc.) so it reaches the header without requiring an +# explicit `#include "config.h"` from secure_zero.hpp. +AC_CHECK_DECLS([explicit_bzero], [], [], [[#include ]]) +AS_IF([test "x$ac_cv_have_decl_explicit_bzero" = "xyes"], + [AM_CXXFLAGS="$AM_CXXFLAGS -DHAVE_EXPLICIT_BZERO" + AM_CFLAGS="$AM_CFLAGS -DHAVE_EXPLICIT_BZERO"]) + # Checks for libmicrohttpd -AC_CHECK_HEADER([microhttpd.h], - AC_CHECK_LIB([microhttpd], [MHD_get_fdset2], - [AC_MSG_CHECKING([for libmicrohttpd >= 0.9.52]) - AC_COMPILE_IFELSE( - [AC_LANG_SOURCE([ - #include - #if (MHD_VERSION < 0x00095102) - #error needs at least version 0.9.52 - #endif - int main () { return 0; } - ])], +if test x"$host" = x"$build"; then + AC_CHECK_HEADER([microhttpd.h], + AC_CHECK_LIB([microhttpd], [MHD_start_daemon], + [AC_MSG_CHECKING([for libmicrohttpd >= 1.0.0]) + AC_COMPILE_IFELSE( + [AC_LANG_SOURCE([ + #include + #if (MHD_VERSION < 0x01000000) + #error needs at least version 1.0.0 + #endif + int main () { return 0; } + ])], + [], + [AC_MSG_ERROR("libmicrohttpd is too old - install libmicrohttpd >= 1.0.0")] + ) + ], + [AC_MSG_ERROR(["libmicrohttpd not found"])] + ), + [AC_MSG_ERROR(["microhttpd.h not found"])] + ) + + # -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" +else + AC_CHECK_HEADER([microhttpd.h], + AC_CHECK_LIB([microhttpd], [MHD_start_daemon], [], - [AC_MSG_ERROR("libmicrohttpd is too old - install libmicrohttpd >= 0.9.52")] - ) - ], - [AC_MSG_ERROR(["libmicrohttpd not found"])] - ), - [AC_MSG_ERROR(["microhttpd.h not found"])] -) + [AC_MSG_ERROR(["libmicrohttpd not found"])] + ), + [AC_MSG_ERROR(["microhttpd.h not found"])] + ) + + # 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" +fi + +AM_CONDITIONAL([COND_CROSS_COMPILE],[test x"$cond_cross_compile" = x"yes"]) +AC_SUBST(COND_CROSS_COMPILE) + +# Check for basic auth v3 support in libmicrohttpd +AC_CHECK_LIB([microhttpd], [MHD_basic_auth_get_username_password3], + [have_bauth="yes"], + [have_bauth="no"; AC_MSG_WARN("libmicrohttpd basic auth v3 support not found. Basic auth will be disabled")]) -CXXFLAGS="-DHTTPSERVER_COMPILATION -D_REENTRANT $LIBMICROHTTPD_CFLAGS $CXXFLAGS" -LDFLAGS="$LIBMICROHTTPD_LIBS $REGEX_LIBS $LD_FLAGS" +# Check for digest auth v3 support in libmicrohttpd +AC_CHECK_LIB([microhttpd], [MHD_digest_auth_check3], + [have_dauth="yes"], + [have_dauth="no"; AC_MSG_WARN("libmicrohttpd digest auth v3 support not found. Digest auth will be disabled")]) + +# Check for WebSocket support in libmicrohttpd_ws +AC_CHECK_HEADER([microhttpd_ws.h], + [AC_CHECK_LIB([microhttpd_ws], [MHD_websocket_stream_init], + [have_websocket="yes"], + [have_websocket="no"; AC_MSG_WARN("libmicrohttpd_ws not found. WebSocket support will be disabled")])], + [have_websocket="no"; AC_MSG_WARN("microhttpd_ws.h not found. WebSocket support will be disabled")]) AC_MSG_CHECKING([whether to build with TCP_FASTOPEN support]) AC_ARG_ENABLE([fastopen], @@ -122,14 +213,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 @@ -169,6 +258,36 @@ AC_MSG_RESULT([$debugit]) AM_LDFLAGS="-lstdc++" if test x"$debugit" = x"yes"; then + AC_DEFINE([DEBUG],[],[Debug Mode]) + # -std=c++20 is injected by AX_CXX_COMPILE_STDCXX into the CXX variable, + # not repeated here. Omitting it avoids a double-standard flag conflict. + 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]) + AM_CXXFLAGS="$AM_CXXFLAGS -O3" + AM_CFLAGS="$AM_CXXFLAGS -O3" +fi + +case $host_os in + darwin* ) + AM_CXXFLAGS="$AM_CXXFLAGS -DDARWIN" + AM_CFLAGS="$AM_CFLAGS -DDARWIN" + ;; + freebsd* ) + AM_LDFLAGS="" + ;; +esac + +AC_MSG_CHECKING([whether to build with coverage information]) +AC_ARG_ENABLE([coverage], + [AS_HELP_STRING([--enable-coverage], + [enable coverage data generation (def=no)])], + [coverit="$enableval"], + [coverit=no]) +AC_MSG_RESULT([$coverit]) + +if test x"$coverit" = x"yes"; then case $host_os in darwin* ) echo "Coverage not supported on OSX" @@ -179,24 +298,46 @@ if test x"$debugit" = x"yes"; then cond_gcov="yes" ;; esac - - AC_DEFINE([DEBUG],[],[Debug Mode]) - AM_CXXFLAGS="$AM_CXXFLAGS -DDEBUG -g -Wall -Wno-uninitialized -O0" - AM_CFLAGS="$AM_CXXFLAGS -DDEBUG -g -Wall -Wno-uninitialized -O0" -else - AC_DEFINE([NDEBUG],[],[No-debug Mode]) - AM_CXXFLAGS="$AM_CXXFLAGS -O3" - AM_CFLAGS="$AM_CXXFLAGS -O3" fi +AC_ARG_ENABLE([[examples]], + [AS_HELP_STRING([[--disable-examples]], [do not build any examples])], , + [enable_examples=yes]) +test "x$enable_examples" = "xno" || enable_examples=yes +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 +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" +fi + +AM_CONDITIONAL([HAVE_DAUTH],[test x"$have_dauth" = x"yes"]) + +if test x"$have_websocket" = x"yes"; then + AM_CXXFLAGS="$AM_CXXFLAGS -DHAVE_WEBSOCKET" + AM_CFLAGS="$AM_CXXFLAGS -DHAVE_WEBSOCKET" + LHT_LIBDEPS="$LHT_LIBDEPS -lmicrohttpd_ws" +fi + +AM_CONDITIONAL([HAVE_WEBSOCKET],[test x"$have_websocket" = x"yes"]) + DX_HTML_FEATURE(ON) DX_CHM_FEATURE(OFF) DX_CHI_FEATURE(OFF) @@ -207,7 +348,11 @@ DX_PDF_FEATURE(OFF) DX_PS_FEATURE(OFF) DX_INIT_DOXYGEN([$PACKAGE_NAME],[doxyconfig.in]) -LDFLAGS="$LDFLAGS -version-number libhttpserver_LDF_VERSION" +# Export the libtool version-number triple so src/Makefile.am can scope it +# to libhttpserver_la_LDFLAGS rather than injecting it into the global LDFLAGS +# (which would affect all link steps, not just the shared library). +LHT_LDF_VERSION="libhttpserver_LDF_VERSION" +AC_SUBST(LHT_LDF_VERSION) AC_SUBST(LHT_LIBDEPS) AC_SUBST(AM_CXXFLAGS) @@ -219,6 +364,29 @@ AC_SUBST(LDFLAGS) AC_SUBST(EXT_LIB_PATH) AC_SUBST(EXT_LIBS) +AC_CONFIG_FILES([test/test_content:test/test_content]) +AC_CONFIG_FILES([test/test_content_2:test/test_content_2]) +AC_CONFIG_FILES([test/test_content_empty:test/test_content_empty]) +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/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]) +# TASK-088: helgrind/drd suppression files must be copied into the build dir +# so VALGRIND_SUPPRESSIONS_FILES (bare filenames, resolved against builddir) +# find them in an out-of-tree build — mirroring libhttpserver.supp above. +AC_CONFIG_FILES([test/valgrind-helgrind.supp:test/valgrind-helgrind.supp]) +AC_CONFIG_FILES([test/valgrind-drd.supp:test/valgrind-drd.supp]) +AC_CONFIG_FILES([examples/cert.pem:examples/cert.pem]) +AC_CONFIG_FILES([examples/key.pem:examples/key.pem]) +AC_CONFIG_FILES([examples/test_content:examples/test_content]) + AC_OUTPUT( libhttpserver.pc Makefile @@ -228,12 +396,18 @@ AC_OUTPUT( examples/Makefile ) -AC_MSG_NOTICE([Configuration Summary: +AC_MSG_NOTICE([Configuration Summary: Operating System: ${host_os} + Host triplet : ${host} Target directory: ${prefix} License : LGPL only Debug : ${debugit} TLS Enabled : ${have_gnutls} + Basic Auth : ${have_bauth} + Digest Auth : ${have_dauth} + WebSocket : ${have_websocket} TCP_FASTOPEN : ${is_fastopen_supported} Static : ${static} + Windows build : ${is_windows} + Build examples : ${enable_examples} ]) diff --git a/custom_iwyu.imp b/custom_iwyu.imp new file mode 100644 index 000000000..e814a3107 --- /dev/null +++ b/custom_iwyu.imp @@ -0,0 +1,17 @@ +[ + { include: ["\"microhttpd.h\"", "private", "", "public"] }, + { include: ["", "private", "", "public"] }, + + { symbol: ["std::exception", "private", "", "public"]}, + { symbol: ["std::shared_ptr", "private", "", "public"]}, + { symbol: ["std::uint16_t", "private", "", "public"]}, + { symbol: ["std::uint64_t", "private", "", "public"]}, + { symbol: ["std::istringstream", "private", "", "public"]}, + { symbol: ["std::stringstream", "private", "", "public"]}, + { symbol: ["std::ifstream", "private", "", "public"]}, + + { symbol: ["uint16_t", "private", "", "public"]}, + { symbol: ["uint64_t", "private", "", "public"]}, + + { symbol: ["MHD_Connection", "private", "", "public"]}, +] diff --git a/docs/architecture/README.md b/docs/architecture/README.md new file mode 100644 index 000000000..b5c395453 --- /dev/null +++ b/docs/architecture/README.md @@ -0,0 +1,25 @@ +# libhttpserver v2.0 — architecture docs + +Architecture references for the v2.0 (`feature/v2.0`) codebase. Every topic is a Markdown page (Mermaid + tables, renders inline on GitHub); the two spatial diagrams additionally have a rich, self-contained HTML companion (no external assets, light/dark aware) for the full colour-coded detail. + +### Diagrams + +| Page | Rich companion | What it shows | +|---|---|---| +| [**class-map.md**](class-map.md) | [`class-map.html`](class-map.html) | `webserver` → `webserver_impl` composition root (5 state collaborators + 7 behavior services + MHD adapter), per-request state, `response_body` hierarchy, filesystem layout | +| [**request-flow.md**](request-flow.md) | [`request-flow.html`](request-flow.html) | the MHD callback spine, body-accumulation loop, WebSocket branch, four-tier route resolution, and where the 11 hook phases fire | + +### Deep dives + +| Page | For | Covers | +|---|---|---| +| [**threading.md**](threading.md) | contributors | DR-008 concurrency contract, full mutex inventory, lock ordering, Helgrind lane | +| [**errors.md**](errors.md) | contributors | DR-009 propagation, every 4xx/5xx origin, the handler-exception path, config knobs | +| [**hooks.md**](hooks.md) | API users | the 11 phases, context fields, short-circuit vs observe, `hook_handle`, recipes | +| [**features.md**](features.md) | packagers / API users | `HAVE_*` detection, gated symbols, `features()`, `feature_unavailable`, build matrix | + +> **Colour language** (shared by the two diagrams): composition-root = blue · state collaborator = amber · behavior service = teal · MHD C-ABI adapter = purple · domain / value type = slate. + +## Keeping these current + +These describe `feature/v2.0` as of the DR-014 decomposition. They are **hand-authored, not generated** — when the composition root's collaborators, the callback spine, the route tiers, or the hook phases change, update the affected Markdown page and, for the two diagrams, its `.html` companion (and redeploy the live Claude artifact to keep the shared link in sync). diff --git a/docs/architecture/class-map.html b/docs/architecture/class-map.html new file mode 100644 index 000000000..7aba25ec2 --- /dev/null +++ b/docs/architecture/class-map.html @@ -0,0 +1,456 @@ + + + + + + + + +libhttpserver v2 — class & filesystem map + + +
+
+

libhttpserver v2.0 · architecture reference

+

Classes, relationships & filesystem — webserver and its collaborators

+

Post-DR-014: webserver is a thin façade over webserver_impl, which is a pure composition root holding 5 state collaborators (own mutex + data) and 7 behavior services (stateless, constructor-injected by const&), plus a static MHD adapter facet. Colour encodes the role; every card carries its header and translation units. Paths are relative to src/.

+ +
+
+ composition root + state collaborator + behavior service + MHD adapter facet + domain / value type +
+ +
+ ◆owns (value / unique_ptr) + ▸holds ref (injected) + ↳inherits + ⌁shared/weak ptr +
+
+
+ + +
+
01

Composition backbone

+

Ownership is strictly linear: create_webserver builds a webserver, which owns one webserver_impl (unique_ptr), which holds all twelve collaborators by value. Services below reference the state collaborators and each other by const&; construction order (state → leaf services → dispatcher → pipeline) is deliberate and enforced by -Wreorder. Every service also holds const webserver_config& (reached via parent->config) — shown as a dashed cfg chip.

+ +
+ create_webserver ──build──▸ webserver + · + webserver ◆ owns ▸ webserver_impl (unique_ptr) · ◆ webserver_config (value) +
+ +
+ webserver_impl composition root + MHD adapter · httpserver/detail/webserver_impl.hpp + +
+
State collaborators — each owns its own mutex(es) + data; no lock nests with another
+
+
+
daemon_lifecycle
+

MHD_Daemon handle, start/stop threading (RAII pthread), option-array + start-flag builders.

+
hpphttpserver/detail/daemon_lifecycle.hppcppdetail/daemon_lifecycle.cpp
+
▸backwebserver_impl*
+
+
+
route_table
+

3-tier route store (exact map · radix trie · regex) + LRU cache, behind route_table_mutex_.

+
hpphttpserver/detail/route_table.hppcppdetail/route_table.cpp
+
◆route_entry◆segment_trie◆route_cache
+
+
+
hook_bus
+

Eleven server-wide phase vectors + alias slots, behind one registration mutex.

+
hpphttpserver/detail/hook_bus.hppcppdetail/hook_bus.cpp
+
+
+
ip_access_control
+

Deny/allow IP sets + their mutexes; classify() for the accept policy.

+
hpphttpserver/detail/ip_access_control.hppcppdetail/ip_access_control.cpp
+
◆ip_representation set
+
+
+
ws_registry HAVE_WEBSOCKET
+

URL → handler map + mutex; hands out shared_ptr copies that survive mid-upgrade unregister.

+
hpphttpserver/detail/ws_registry.hppcppdetail/ws_registry.cpp
+
⌁websocket_handler
+
+
+
+ +
+
Behavior services — stateless; hold only const& to state + each other; form a DAG, no back-pointer
+
+
+
error_pages leaf
+

Synthesises 404 / 405 / 500 responses. Reads only the config bag + cc->request.

+
hpphttpserver/detail/error_pages.hppcppdetail/error_pages.cpp
+
cfg
+
+
+
hook_dispatcher
+

Dispatch-time firing: the eleven fire_* phases + four gated helpers.

+
hpphttpserver/detail/hook_dispatcher.hppcppdetail/hook_dispatcher.cpp
+
▸hook_buscfg
+
+
+
response_materializer
+

http_response → MHD_Response: materialize, decorate, kind-dispatched queue, response_sent + fallback chain.

+
hpphttpserver/detail/response_materializer.hppcppdetail/response_materializer.cpp
+
▸error_pages▸hook_dispatchercfg
+
+
+
upload_pipeline
+

Multipart / file-upload: post-iterator handling, upload streams, form args.

+
hpphttpserver/detail/upload_pipeline.hppcppdetail/upload_pipeline.cpp
+
cfg
+
+
+
websocket_upgrader HAVE_WEBSOCKET
+

RFC-6455 handshake + per-connection frame receive loop.

+
hpphttpserver/detail/websocket_upgrader.hppcppdetail/websocket_upgrader.cpp
+
▸ws_registry
+
+
+
request_dispatcher
+

Routing + auth + handler invocation: finalize_answer / resolve_resource / dispatch_resource_handler.

+
hpphttpserver/detail/request_dispatcher.hppcppdetail/request_dispatcher.cpp
+
▸route_table▸hook_dispatcher▸error_pages▸response_materializer▸ws_upgradercfg
+
+
+
request_pipeline
+

MHD re-entrant body-accumulation state machine: first_step / second_step / complete_request.

+
hpphttpserver/detail/request_pipeline.hppcppdetail/request_pipeline.cpp
+
▸hook_dispatcher▸request_dispatchercfg
+
+
+
+ +
+

▪ MHD adapter facet — static C-ABI trampolines on webserver_impl (not an 8th service)

+

libmicrohttpd calls these by function-pointer address. Each casts cls → webserver → impl_, marshals the C ABI (sockaddr → peer_address, con_cls/socket_context lifetime), and delegates all behavior to a service. They must live on the impl to reach every collaborator — the trampoline is the adapter.

+
+ answer_to_connection ▸pipeline_ + connection_notify ▸hooks_dispatch_ + policy_callback ▸acl_ + request_completed ▸hooks_dispatch_ + post_iterator ▸upload_ + uri_log + error_log + unescaper_func + psk / sni_cert GnuTLS +
+

All trampolines live in one adapter TU: detail/webserver_callbacks.cpp

+
+
+ +
+
Translation-unit map — one-class-per-file shape: webserver = a single façade TU (webserver.cpp, 552 SLOC); webserver_impl = 2 TUs (composition root + MHD adapter, 108 + 381 SLOC). Both webserver.cpp and webserver_callbacks.cpp sit under the 600-SLOC façade/adapter carve-out (other files: 500) — check-file-size gate PASSES. No file spans both classes. Headers: webserver.hpp (+ fragments webserver_routes / runtime / websocket / hooks.hpp) · detail/webserver_impl.hpp (+ webserver_impl_dispatch.hpp)
+
+
webserver.cpp webserver — entire public API
ctor/dtor · features · lifecycle (start/stop/run/quiesce/get_fdset · allow/deny/remove_ip) · register/unregister_* · on_*/route · register/unregister_ws_resource · add_hook · install_* v1 alias hooks (auth/404/405/log_access) · stop_and_wait  552 SLOC — within the 600 façade carve-out ✓
+ +
detail/webserver_impl.cpp webserver_impl · composition root
webserver_impl ctor/dtor — collaborator + service wiring (the DR-014 DAG) · serialize_allow_methods · prepare_or_create_lambda_shim / commit_handlers_to_shim (on_*/route shim glue)
+
detail/webserver_callbacks.cpp webserver_impl · MHD adapter
all C-ABI trampolines — answer_to_connection · connection_notify · policy_callback · request_completed · post_iterator · uri_log · error_log · unescaper_func · psk / sni_cert (GnuTLS) + resolve_method_callback · should_skip_auth · normalize_path · peer_address adapter  (381 SLOC)
+ +
detail/webserver_body_pipeline.cpp free fns
debug_dump_request_body_opted_in · maybe_warn_debug_dump_request_body
+
detail/dispatch_util.cpp free fns
detail::log_dispatch_error(const webserver_config&, string_view) — hpp: httpserver/detail/dispatch_util.hpp
+
string_utilities.cpp free fns
is_valid_hex · hex_char_to_val · … — hpp: httpserver/string_utilities.hpp
+
+
+
+ + +
+
02

Request · response · resource layer

+

The per-request objects the services operate on. connection_context is the dispatch-state blackboard threaded through the pipeline via MHD's con_cls (allocated in uri_log, freed in request_completed); the arena that backs each http_request lives on connection_state (MHD's socket_context, one per keep-alive connection).

+
+
+
http_request
+

Per-request inputs, PIMPL over an arena-allocated impl (custom deleter).

+
hpphttpserver/http_request.hppcpphttp_request.cpp · http_request_auth.cpp
+
◆http_request_impl
+
+
+
http_request_impl PIMPL
+

~40 members, 5 concerns, one arena-allocated unit (not decomposed — see §4.2).

+
hpphttpserver/detail/http_request_impl.hppcppdetail/http_request_impl.cpp · …_args.cpp · …_tls.cpp
+
◆file_info◆cookie
+
+
+
http_response final
+

Sealed value type; owns one polymorphic body in a 64-byte SBO buffer. Built only via factories.

+
hpphttpserver/http_response.hppcpphttp_response.cpp · http_response_factories.cpp
+
◆response_body (SBO)◆cookie
+
+
+
http_resource
+

User-subclassable handler base; lazy per-resource hook table + allowed-method cache.

+
hpphttpserver/http_resource.hppcpphttp_resource.cpp
+
⌁resource_hook_table◆method_set
+
+
+
lambda_resource final
+

↳ http_resource. Header-only shim backing on_*/route lambda registration.

+
hpphttpserver/detail/lambda_resource.hppcpp— (inline)
+
↳http_resource
+
+
+
connection_context struct
+

Per-request dispatch blackboard (con_cls). Carries request, response, resolved handler + ptr-to-member callback, upload stream, post-processor.

+
hpphttpserver/detail/connection_context.hppcpp— (header-only)
+
◆http_request◆http_response?⌁http_resource▸webserver*
+
+
+
connection_state struct
+

Per-MHD-connection PMR arena + byte buffer; reset (release + secure-zero) between keep-alive requests.

+
hpphttpserver/detail/connection_state.hppcpp— (inline)
+
+
+
http_endpoint
+

Parsed registration path (segments, params, compiled regex).

+
hpphttpserver/detail/http_endpoint.hppcppdetail/http_endpoint.cpp
+
+
+
file_info
+

Uploaded-file descriptor (size, path, content-type).

+
hpphttpserver/file_info.hppcppfile_info.cpp
+
+
+
+ + +
+
03

Response body hierarchy

+

The only real inheritance tree in the library. http_response owns exactly one response_body subclass, stored inline in its SBO buffer (each concrete body is ≤ 64 bytes, noexcept-move via move_into). All declared in one header, defined in one TU.

+
+
response_body abstract
+
hpphttpserver/detail/body.hppcppdetail/body.cpp
+
+
empty_response_bodyint flags
+
string_response_bodystd::string
+
file_response_bodypath / fd / size
+
iovec_response_bodyvector<iovec_entry>
+
pipe_response_bodyfd
+
deferred_response_bodyproducer fn + trampoline
+
digest_challenge_response_bodyunique_ptr<params>
+
+
+
+ + +
+
04

Route-table internals

+

Value types owned by route_table — the three tiers plus the LRU cache. Mostly header-only templates and PODs.

+
+
route_entry struct · ◆ method_set · ⌁ shared_ptr<http_resource>
hpp httpserver/detail/route_entry.hpp
+
segment_trie<T> + node / match · header-only template
hpp httpserver/detail/segment_trie.hpp
+
route_cache + cache_key / cache_value · header-only
hpp httpserver/detail/route_cache.hpp
+
route_tier_result struct + helpers
hpp httpserver/detail/route_tier.hpp · helpers in detail/route_table.cpp
+
+
+ + +
+
05

Hook system

+

hook_bus (server-wide, §01) and resource_hook_table (per-resource) store the registered callbacks; hook_dispatcher fires them; hook_handle is the RAII receipt that removes them. Context structs are passed to user callbacks by pointer only.

+
+
hook_handle move-only · ▸ webserver_impl* · ⌁ weak resource_hook_table
hpp httpserver/hook_handle.hpp · cpp hook_handle.cpp
+
resource_hook_table per-resource · 5 phase vectors
hpp httpserver/detail/resource_hook_table.hpp · cpp resource_hook_table.cpp
+
hook context structs accept · request_received · body_chunk · route_resolved · before/after_handler · handler_exception · response_sent · request_completed · connection_open/close · route_descriptor
hpp httpserver/hook_context.hpp
+
peer_address struct POD
hpp httpserver/hook_context.hpp · cpp peer_address.cpp
+
hook_action short-circuit payload · header-only
hpp httpserver/hook_action.hpp
+
+
+ + +
+
06

Config, builders, value & utility types

+

The config bag every service reads, the fluent builders, and the standalone value/utility types.

+
+
webserver_config struct · the single config bag (const& into every service)
hpp httpserver/create_webserver.hpp · cpp create_webserver.cpp
+
create_webserver fluent builder · ◆ webserver_config
hpp httpserver/create_webserver.hpp (+ create_webserver_setters.hpp) · cpp create_webserver.cpp
+
create_test_request test-request builder
hpp httpserver/create_test_request.hpp · cpp create_test_request.cpp
+
websocket_handler abstract user base
hpp httpserver/websocket_handler.hpp · cpp websocket_handler.cpp
+
websocket_session live upgraded connection
hpp httpserver/websocket_handler.hpp · cpp websocket_handler.cpp
+
ip_representation struct · octets + mask
hpp httpserver/ip_representation.hpp · cpp detail/ip_representation.cpp
+
cookie final · RFC 6265
hpp httpserver/cookie.hpp · cpp cookie.cpp
+
method_set struct · method bitmask (value, held widely)
hpp httpserver/http_method.hpp (inline)
+
http_arg_value multi-value arg view · header-only
hpp httpserver/http_arg_value.hpp
+
iovec_entry POD {base, len}
hpp httpserver/iovec_entry.hpp
+
http_utils static utils + header/arg comparators
hpp httpserver/http_utils.hpp · cpp http_utils.cpp
+
feature_unavailable ↳ std::runtime_error
hpp httpserver/feature_unavailable.hpp
+
+
+ +
+

Reading the graph. Solid ownership (◆) flows one way and top-down: webserver → webserver_impl → collaborators (by value) → their owned value types. Behavior services never own state — they hold const& (▸) into the state collaborators and each other, wired once in the webserver_impl constructor, forming an acyclic DAG with no back-pointer (the sole exception is daemon_lifecycle, which needs webserver_impl* to read broad config while building the MHD option array). The static MHD trampolines are the only code that touches libmicrohttpd's C ABI. Header convention: public surface under src/httpserver/, internal detail headers under src/httpserver/detail/ (never installed), implementations under src/ and src/detail/.

+
+
+ + + + diff --git a/docs/architecture/class-map.md b/docs/architecture/class-map.md new file mode 100644 index 000000000..72afb49ca --- /dev/null +++ b/docs/architecture/class-map.md @@ -0,0 +1,126 @@ +# Class, relationship & filesystem map + +> How the classes fit together and where they live on disk, for libhttpserver v2.0. +> Quick-view below; the full colour-coded page with every class card and file location is **[`class-map.html`](class-map.html)** (open in a browser). + +Post-**DR-014**, `webserver` is a thin façade over `webserver_impl`, which is a **pure composition root** holding **5 state collaborators** (own their mutexes + data) and **7 behavior services** (stateless; hold `const&` into state and each other), plus a **static MHD adapter facet** (the C-ABI trampolines). Ownership is strictly linear and top-down; services form an acyclic DAG with no back-pointer (the sole exception: `daemon_lifecycle` needs `webserver_impl*` to read broad config while building the MHD option array). + +> **Colour language** (used by the HTML pages): composition-root = blue · state collaborator = amber · behavior service = teal · MHD C-ABI adapter = purple · domain / value type = slate. + +## Composition backbone + +Stereotypes encode the role: `<>` = state collaborator (owns mutex + data), `<>` = behavior service (stateless), `<>` = MHD C-ABI facet. Solid diamond (`*--`) = owns by value; dashed arrow (`..>`) = holds `const&` reference. + +```mermaid +classDiagram + direction LR + class create_webserver + class webserver + class webserver_impl { + <> + } + create_webserver --> webserver : builds + webserver *-- webserver_impl : owns + + class daemon_lifecycle { + <> + } + class route_table { + <> + } + class hook_bus { + <> + } + class ip_access_control { + <> + } + class ws_registry { + <> + } + webserver_impl *-- daemon_lifecycle + webserver_impl *-- route_table + webserver_impl *-- hook_bus + webserver_impl *-- ip_access_control + webserver_impl *-- ws_registry + + class error_pages { + <> + } + class hook_dispatcher { + <> + } + class response_materializer { + <> + } + class upload_pipeline { + <> + } + class websocket_upgrader { + <> + } + class request_dispatcher { + <> + } + class request_pipeline { + <> + } + webserver_impl *-- error_pages + webserver_impl *-- hook_dispatcher + webserver_impl *-- response_materializer + webserver_impl *-- upload_pipeline + webserver_impl *-- websocket_upgrader + webserver_impl *-- request_dispatcher + webserver_impl *-- request_pipeline + + hook_dispatcher ..> hook_bus + response_materializer ..> error_pages + response_materializer ..> hook_dispatcher + websocket_upgrader ..> ws_registry + request_dispatcher ..> route_table + request_dispatcher ..> hook_dispatcher + request_dispatcher ..> error_pages + request_dispatcher ..> response_materializer + request_dispatcher ..> websocket_upgrader + request_pipeline ..> hook_dispatcher + request_pipeline ..> request_dispatcher +``` + +`ws_registry` + `websocket_upgrader` are wired only on `HAVE_WEBSOCKET` builds. `route_table` owns `route_entry` / `segment_trie` / `route_cache`; `hook_bus` holds the 11 server-wide phase vectors; `response_materializer` turns `http_response` into an `MHD_Response`; `request_pipeline` is the re-entrant body-accumulation state machine. The mutexes each state collaborator owns are catalogued in [threading.md](threading.md). + +## Per-request / per-connection state + +The objects threaded through the services during a request: + +```mermaid +classDiagram + direction LR + class connection_context + class connection_state + class http_request + class http_response + class http_resource + class response_body { + <> + } + + connection_context *-- http_request : owns + connection_context *-- http_response : owns optional + connection_context ..> http_resource : weak_ptr + http_response *-- response_body : owns 64B SBO + response_body <|-- empty_response_body + response_body <|-- string_response_body + response_body <|-- file_response_body + response_body <|-- iovec_response_body + response_body <|-- pipe_response_body + response_body <|-- deferred_response_body + response_body <|-- digest_challenge_response_body +``` + +`connection_context` is MHD's `*con_cls` (the dispatch blackboard, allocated in `uri_log`); `connection_state` is MHD's `socket_context` (the per-keep-alive-connection PMR arena). `http_response` stores one `response_body` subclass inline in a 64-byte SBO buffer. + +## Filesystem convention + +Public surface in `src/httpserver/` (installed); internal detail headers in `src/httpserver/detail/` (never installed); implementations in `src/` and `src/detail/`. `webserver` = one façade TU (`src/webserver.cpp`); `webserver_impl` = two TUs (`src/detail/webserver_impl.cpp` composition root + `src/detail/webserver_callbacks.cpp` MHD adapter). Both `webserver.cpp` and `webserver_callbacks.cpp` sit under the 600-SLOC façade/adapter carve-out of the `check-file-size` gate (others: 500). The full per-class header/cpp locations are in [`class-map.html`](class-map.html). + +--- +*See also: [request-flow](request-flow.md) (how these classes collaborate per request) · [threading.md](threading.md) (the mutexes they own).* diff --git a/docs/architecture/errors.md b/docs/architecture/errors.md new file mode 100644 index 000000000..52b437951 --- /dev/null +++ b/docs/architecture/errors.md @@ -0,0 +1,88 @@ +# Error propagation & status codes (DR-009) + +> Where every error status comes from, how handler exceptions become responses, and what you can customize. +> Decision record: [`specs/architecture/11-decisions/DR-009.md`](../../specs/architecture/11-decisions/DR-009.md) · cross-cutting §5.2: [`05-cross-cutting.md`](../../specs/architecture/05-cross-cutting.md). + +## The contract + +DR-009 chose **Option 1 — no throw-as-status idiom.** There is no `http_error` type. To return a 404/400 you build a response by value: + +```cpp +return http_response::empty().with_status(404); +``` + +Any **uncaught exception** from a handler becomes a **500**, routed through an optional `internal_error_handler` escape hatch. The six-point rule: + +1. Handler throws `std::exception` → caught, logged via `log_error`, `internal_error_handler` invoked with `e.what()`. **Default body is the fixed string `"Internal Server Error"`** (CWE-209) — the verbatim message reaches only the log and a configured handler. +2. Handler throws non-`std::exception` → `catch (...)`, message sentinel `"unknown exception"`, same fixed default body. +3. Library-internal dispatch exception (allocation, body materialization) → same path. +4. `internal_error_handler` itself throws → logged, hardcoded **empty-body 500**. +5. `feature_unavailable` is a plain `std::runtime_error` — no special status mapping. +6. No throw-as-status. + +**No-throw boundary:** the exception→status translation is entirely internal C++; nothing new crosses the ABI. A response always reaches MHD — the four terminal producers are `noexcept` or exception-contained. + +## Status origins + +| Status | Producer | Trigger | Customize with | +|---|---|---|---| +| **404** | `error_pages::not_found_page` | route lookup miss in `finalize_answer` (also the `invalid_argument` fallback arm of `get_raw_response_with_fallback`) | `not_found_handler` | +| **405** | `error_pages::method_not_allowed_page` | `is_allowed(method)` false in `dispatch_resource_handler`; `Allow:` header from `hrm->get_allow_header()` | `method_not_allowed_handler` | +| **500** (default `"Internal Server Error"`) | `error_pages::internal_error_page` (`force_our=false`) | handler/dispatch exception, null-materialize, `-1` sentinel response | `internal_error_handler`, `expose_exception_messages` | +| **500** (body = `e.what()`) | same, when `expose_exception_messages==true` | dev opt-in only | — | +| **500** (hardcoded empty body) | `internal_error_page(force_our=true)` | double-fault: user handler threw, or belt-and-suspenders sites | — | +| **401** (Basic / scheme) | `http_response::unauthorized(scheme, realm, body)` | user-returned; builds `WWW-Authenticate: realm="…"` (control-char rejection, quoted-string escaping) | your handler | +| **401** (Digest) | `http_response::unauthorized(digest_challenge)` → `MHD_queue_auth_required_response3` | user-returned; `body_kind::digest_challenge` (needs `HAVE_DAUTH`) | your handler | +| **400** (bad WS handshake) | `websocket_upgrader::try_handle` | `Upgrade: websocket` present but `validate_websocket_handshake` fails (bad `Connection` / `Sec-WebSocket-Version` ≠ 13 / empty key) | — | +| **101** (success, not an error) | `complete_websocket_upgrade` | successful WebSocket upgrade | — | + +Default bodies (`constants.hpp`): `"Not Found"`, `"Method not Allowed"`, `"Internal Server Error"`. + +## The handler-exception path + +```mermaid +flowchart TD + H["handler throws"] --> C{"catch in
dispatch_resource_handler"} + C --> L["log_dispatch_error · verbatim to log_error"] + L --> HDE{"handler_exception hooks
or alias present?"} + HDE -- yes --> FS["fire_handler_exception
server-wide, then per-route"] + FS --> R{"a hook returned
respond_with?"} + R -- yes --> OUT["that response is sent"] + R -- no --> FORCE["internal_error_page force_our
empty-body 500"] + HDE -- no --> SAFE["run_internal_error_handler_safely"] + SAFE --> IEH{"internal_error_handler set?"} + IEH -- yes --> UH["user handler(request, message)"] + IEH -- no --> EXP{"expose_exception_messages?"} + EXP -- yes --> BODYW["body = message"] + EXP -- no --> BODYD["body = Internal Server Error"] + UH -. throws .-> FORCE +``` + +Notable details: + +- **`-1` sentinel:** `http_response::status_code_` defaults to `-1`. A returned response with `get_status()==-1` is treated as null and rerouted through `run_internal_error_handler_safely(…, "handler returned null response")`. +- **A throwing `handler_exception` hook does not abort the chain** — it is logged and the next hook runs (the chain's job is recovery). Every *other* phase's throwing hook is routed back through the normal DR-009 500 path. +- **`run_internal_error_handler_safely`** wraps the user `internal_error_handler` in try/catch; if it throws, it logs and falls back to the hardcoded empty-body 500. No exception escapes. +- **`log_dispatch_error`** (`noexcept`) is the single internal logging channel: no-op if `log_error` is unset, otherwise forwards the message **verbatim** (flag-independent) inside a `try/catch` that swallows a misbehaving logger. + +## Configuration + +| Knob | Signature / default | Effect | +|---|---|---| +| `not_found_handler` | `std::function`, default none | replaces the 404 body | +| `method_not_allowed_handler` | `std::function`, default none | replaces the 405 body (`Allow:` still added) | +| `internal_error_handler` | `std::function`, default none | invoked with the originating message for 500 | +| `expose_exception_messages` | `bool`, default `false` | **dev only** — default-500 body becomes the originating message instead of `"Internal Server Error"` (CWE-209) | +| `expose_credentials_in_logs` | `bool`, default `false` | when `true`, `http_request::operator<<` streams passwords / `Authorization` / cookies verbatim instead of `` (CWE-312/532) | + +**Message-exposure matrix:** the verbatim message always goes to `log_error` and to a configured `internal_error_handler`; it reaches the **wire** only under `expose_exception_messages(true)`. + +## `feature_unavailable` + +`class feature_unavailable : public std::runtime_error` — always compiled, regardless of `HAVE_*`. Thrown when a feature-gated API is used on a build without that feature (see [features & build matrix](features.md)). Its reach depends on the call site: + +- Thrown at **`webserver` construction** or from **setup / registration** APIs → propagates to the **caller** (your application code). +- Thrown from **inside a handler** (e.g. `unauthorized(digest_challenge)` on a non-digest build, or a `websocket_session` send) → it is a normal `std::exception` on the dispatch path → generic **500** to the client, unless an `internal_error_handler` special-cases it. + +--- +*See also: [request-flow](request-flow.md) (where 404/405/500 sit in the dispatch sequence) · [hooks cookbook](hooks.md) (the `handler_exception` phase).* diff --git a/docs/architecture/features.md b/docs/architecture/features.md new file mode 100644 index 000000000..9fd9cf0bc --- /dev/null +++ b/docs/architecture/features.md @@ -0,0 +1,83 @@ +# Feature availability & build matrix + +> Which capabilities compile in, how they're detected, and what happens when they're absent. +> Build & packaging spec: [`specs/architecture/08-build-and-packaging.md`](../../specs/architecture/08-build-and-packaging.md). + +## The design rule + +**The public symbol surface is identical on every build.** Every feature-gated method is always declared and linkable — only the method *body* branches on the build macro, either returning a documented sentinel or throwing `feature_unavailable`. The four `HAVE_*` macros are pushed via compiler `-D` flags (not a `config.h`), so no consumer include is required. + +## The four features + +```mermaid +flowchart LR + subgraph det["Build detection · configure.ac"] + G["gnutls/gnutls.h"] --> HG["HAVE_GNUTLS"] + B["MHD_basic_auth_get_username_password3"] --> HB["HAVE_BAUTH"] + D["MHD_digest_auth_check3"] --> HD["HAVE_DAUTH"] + W["microhttpd_ws.h + MHD_websocket_stream_init"] --> HW["HAVE_WEBSOCKET"] + end + HG --> TLS["TLS · client certs · SNI · PSK"] + HB --> BA["Basic auth"] + HD --> DA["Digest auth"] + HW --> WS["WebSocket"] +``` + +| Feature | Detected by (`configure.ac`) | Gated symbols | `features()` bool | When absent | +|---|---|---|---|---| +| **TLS / HTTPS** | `AC_CHECK_HEADER(gnutls/gnutls.h)` → `HAVE_GNUTLS` | `http_request_impl_tls.cpp` (`get_tls_session`, `populate_all_cert_fields`), `http_request` cert accessors, `sni_credentials_cache`, `sni_cert_callback_func`, `psk_cred_handler_func`, GnuTLS MHD options | `tls` | `use_ssl(true)` → **ctor throws** `feature_unavailable("tls","HAVE_GNUTLS")`; cert accessors return `false`/empty/`-1`; `cred_type`/PSK/SNI options silently dropped | +| **Basic auth** | `AC_CHECK_LIB(microhttpd, MHD_basic_auth_get_username_password3)` → `HAVE_BAUTH` | `fetch_user_pass`, `get_user`/`get_pass` bodies | `basic_auth` | `basic_auth(true)` → **ctor throws** `("basic_auth","HAVE_BAUTH")`; `get_user`/`get_pass` → empty | +| **Digest auth** | `AC_CHECK_LIB(microhttpd, MHD_digest_auth_check3)` → `HAVE_DAUTH` | `digest_opaque_`, `check_digest_auth`, `check_digest_auth_digest`, `get_digested_user`, `unauthorized(digest_challenge)`, `digest_challenge_response_body`, digest MHD options | `digest_auth` | `digest_auth(true)` → **ctor throws** `("digest_auth","HAVE_DAUTH")`; `unauthorized(digest_challenge)` → throws same; `check_*` → `WRONG_HEADER`; `get_digested_user` → empty | +| **WebSocket** | `AC_CHECK_HEADER(microhttpd_ws.h)` + `AC_CHECK_LIB(microhttpd_ws, MHD_websocket_stream_init)` → `HAVE_WEBSOCKET` (adds `-lmicrohttpd_ws`) | `websocket_upgrader`, real `websocket_session`, `websocket_handler` hooks, populated `ws_registry`, `register_ws_resource`/`unregister_ws_resource`, `MHD_ALLOW_UPGRADE` | `websocket` | `register_ws_resource`/`unregister_ws_resource` **throw** `("websocket","HAVE_WEBSOCKET")`; session send/close throw same; handler hooks no-op | + +Each macro is injected into `AM_CXXFLAGS`/`AM_CFLAGS` as `-D` (with a matching `AM_CONDITIONAL`); none is written to `config.h`. + +### Other build-time switches + +| Macro | Detection | Gates | +|---|---|---| +| `MHD_OPTION_HTTPS_CERT_CALLBACK` | MHD-provided | SNI callback wiring (additionally requires `HAVE_GNUTLS`) | +| `MHD_VERSION` | MHD header | `http_utils.cpp` compat shim for pre-0.9.74 status-code spellings | +| `HAVE_EXPLICIT_BZERO` | `AC_CHECK_DECLS([explicit_bzero])` | `secure_zero.hpp` implementation (internal) | +| `USE_FASTOPEN` | kernel `TCP_FASTOPEN` probe (`--enable-fastopen`) | `MHD_USE_TCP_FASTOPEN` daemon flag | + +**Minimum libmicrohttpd: 1.0.0** — `configure.ac` hard-errors below `MHD_VERSION 0x01000000`. + +## Runtime feature reporting + +- **`webserver::features()`** (`noexcept`) returns `{basic_auth, digest_auth, tls, websocket}`, each a `constexpr` resolved purely from the build macros. It reflects the **library's** build flags, not the caller's — the body lives in `webserver.cpp` precisely so consumers see the shipped library's capabilities. It does **not** query MHD. +- **`http::http_utils::is_feature_supported(int)`** is the only *runtime* MHD query — wraps `MHD_is_feature_supported((enum MHD_FEATURE)…)`; independent of the four `HAVE_*` macros. +- **`http::http_utils::get_mhd_version()`** returns MHD's version string. + +## `feature_unavailable` throw sites + +`class feature_unavailable : public std::runtime_error` (always compiled). `what()` = `"feature '' unavailable: built without "`. + +- `webserver::webserver` ctor — validated **lazily at construction** (never at the setter): `use_ssl` w/o GnuTLS, `basic_auth` w/o BAuth, `digest_auth` w/o DAuth. +- `http_response::unauthorized(digest_challenge)` w/o `HAVE_DAUTH`. +- `webserver::register_ws_resource` / `unregister_ws_resource` and every `websocket_session` send/close w/o `HAVE_WEBSOCKET`. + +Setters never throw — they only mutate config; validation is deferred. So `use_ssl(true)` on a GnuTLS-off build compiles and returns fine, then throws when you construct the `webserver`. + +## Builder setters that depend on a feature + +| Setter | Feature | If feature absent | +|---|---|---| +| `use_ssl(bool)` | HAVE_GNUTLS | ctor throws | +| `https_mem_key/cert/trust` (+ `raw_*`), `https_mem_dhparams`, `https_key_password`, `https_priorities[_append]` | HAVE_GNUTLS (via `use_ssl`) | wired only under `use_ssl`; otherwise inert | +| `cred_type(cred_type_T)` | HAVE_GNUTLS | option added only under `HAVE_GNUTLS`; else ignored (ctor throws first if `use_ssl` also set) | +| `psk_cred_handler(cb)` | HAVE_GNUTLS + `use_ssl` | ignored otherwise | +| `sni_callback(cb)` | HAVE_GNUTLS + cert-callback + `use_ssl` | ignored otherwise | +| `digest_auth_random(str)`, `nonce_nc_size` | HAVE_DAUTH | option added only under `HAVE_DAUTH`; else ignored | +| `basic_auth(bool)` | HAVE_BAUTH | ctor throws if enabled | +| `digest_auth(bool)` | HAVE_DAUTH | ctor throws if enabled | + +`basic_auth`/`digest_auth` default to `true` **only** when their macro is defined, so an auth-off build defaults the flag off and never throws spuriously. + +## Build systems + +- **Autotools is the only build with capability detection**: `configure.ac` + `src/Makefile.am`. All four `HAVE_*` and the switches above are probed here. +- **CMake support is consumer-side only**: `cmakemodule/FindLibHttpServer.cmake` is a plain find-module (locates headers + library); it does no capability detection and does not build the library. There is no top-level `CMakeLists.txt`. + +--- +*See also: [errors](errors.md) (`feature_unavailable`'s reach — caller vs client) · [class map](class-map.md) (the gated collaborators, marked `HAVE_WEBSOCKET`).* diff --git a/docs/architecture/hooks.md b/docs/architecture/hooks.md new file mode 100644 index 000000000..7a3061e76 --- /dev/null +++ b/docs/architecture/hooks.md @@ -0,0 +1,101 @@ +# Hooks cookbook + +> A user-facing reference for the 11 lifecycle hook phases: what each observes, which can short-circuit, and how to register them. +> Component spec: [`specs/architecture/04-components/hooks.md`](../../specs/architecture/04-components/hooks.md) · mechanics in [request-flow](request-flow.md). + +## Where the phases fire + +```mermaid +flowchart LR + O[connection_opened] --> A[accept_decision] --> RR[request_received] --> BC[body_chunk] --> RES[route_resolved] --> BH[before_handler] --> HX[handler_exception] --> AH[after_handler] --> RS[response_sent] --> RC[request_completed] --> CC[connection_closed] +``` + +Five phases can **short-circuit** (return `hook_action::respond_with(...)` to end the request early): `request_received`, `body_chunk`, `before_handler`, `handler_exception`, `after_handler`. The rest are **observe-only** (return `void`; the chain always runs to completion). + +## Phase catalog + +| # | Phase | Context struct | Key fields | Kind | Scope | +|---|---|---|---|---|---| +| 0 | `connection_opened` | `connection_open_ctx` | `peer_address peer` | observe | server | +| 1 | `accept_decision` | `accept_ctx` | `peer_address peer` · `bool accepted` · `optional reason` | observe | server | +| 2 | `request_received` | `request_received_ctx` | `http_request* request` · `steady_clock::time_point received_at` | **short-circuit** | server | +| 3 | `body_chunk` | `body_chunk_ctx` | `http_request* request` · `span chunk` · `uint64_t offset` · `bool is_final` | **short-circuit** | server | +| 4 | `route_resolved` | `route_resolved_ctx` | `const http_request* request` · `optional matched` · `const http_resource* resource` | observe | server | +| 5 | `before_handler` | `before_handler_ctx` | `http_request* request` · `optional matched` · `http_method method` · `const http_resource* resource` | **short-circuit** | server **+ route** | +| 6 | `handler_exception` | `handler_exception_ctx` | `const http_request* request` · `exception_ptr exception` · `string_view message` | **short-circuit** | server **+ route** | +| 7 | `after_handler` | `after_handler_ctx` | `const http_request* request` · `http_response* response` (mutable) | **short-circuit / replace** | server **+ route** | +| 8 | `response_sent` | `response_sent_ctx` | `const http_request* request` · `const http_response* response` · `int status` · `size_t bytes_queued` · `nanoseconds elapsed` | observe | server **+ route** | +| 9 | `request_completed` | `request_completed_ctx` | `const http_request* request` · `const http_response* resp` · `bool succeeded` · `steady_clock::duration duration` | observe | server **+ route** | +| 10 | `connection_closed` | `connection_close_ctx` | `peer_address peer` | observe | server | + +**All pointers are valid only for the synchronous duration of the hook call — never capture them past return.** + +Shared structs: +- `peer_address` — `family fam` (`unspec`/`ipv4`/`ipv6`) · `array bytes` (network order) · `uint16_t port` (host order) · `string to_string()`. +- `route_descriptor` — `string_view path_template` (per-request storage — do not capture) · `method_set methods` · `bool is_prefix`. + +### `hook_action` (the short-circuit payload) + +Move-only wrapper over `optional`: + +| Method | Meaning | +|---|---| +| `hook_action::pass()` | continue the chain | +| `hook_action::respond_with(http_response r)` | short-circuit; `r` is sent, later hooks in the phase skip | +| `is_pass()` | true if no payload | +| `take_response() &&` | consume the payload (precondition `!is_pass()`) | + +**`after_handler` can replace the response two ways:** mutate in place via the mutable `response*` (rewrite headers/status, keep body), **or** `respond_with(r2)` to fully replace it — the resource's reply is discarded and never hits the wire. + +## Registering hooks + +**Server-wide** — `webserver::add_hook(phase, fn)`, all 11 phases. The `phase` argument is validated against the overload's signature at runtime (`std::invalid_argument` on mismatch or empty `fn`). Returns a `hook_handle`. + +```cpp +// observe: void(const Ctx&) ; short-circuit: hook_action(Ctx&) +auto h = ws.add_hook(hook_phase::response_sent, + std::function( + [](const response_sent_ctx& c){ /* log c.status, c.bytes_queued, c.elapsed */ })); +``` + +**Per-route** — `http_resource::add_hook(phase, fn)`, only the **5 post-route-resolution** phases: `before_handler`, `handler_exception`, `after_handler`, `response_sent`, `request_completed`. Any other phase throws `std::invalid_argument`. Per-route hooks fire **after** the server-wide chain at the same phase, and only if that chain did not short-circuit. The per-route table is lazily allocated on first `add_hook`; copying an `http_resource` shares the same table. + +**Execution order at a phase:** server-wide (registration order) → per-route (registration order); a short-circuit skips everything after it. + +### `hook_handle` — the RAII receipt + +Move-only; dropping an **armed** handle removes the registration. + +| Operation | Effect | +|---|---| +| destructor | calls `remove()` unless detached / moved-from / already removed | +| `remove()` | idempotent; erases the registration, then disarms | +| `std::move(h).detach()` | keeps the registration for the server's lifetime; returns an inert receipt | + +⚠️ A server-wide handle holds a **non-owning `webserver_impl*`** — an armed handle must not outlive its `webserver` (use-after-free, CWE-416). Per-route handles hold a `weak_ptr`; if the resource dies first, `remove()` becomes a safe no-op. + +## v1 compatibility aliases + +The old install-time callbacks map onto phases (all wired at construction, immutable afterward — use `add_hook` for runtime registration): + +| v1 setter | Phase | Notes | +|---|---|---| +| `log_access(fn)` | `response_sent` | dedicated `log_access_alias_` slot; fires after user `response_sent` hooks | +| `internal_error_handler(fn)` | `handler_exception` | last-position fallback (`handler_exception_alias_`) before the hardcoded 500 | +| `auth_handler(fn)` | `before_handler` | registered **first**; **fail-closed** — an exception in the auth callable short-circuits with **500** directly, not via `handler_exception` | +| `method_not_allowed_handler(fn)` | `before_handler` | registered second; 405 + `Allow` when method not permitted | +| `not_found_handler(fn)` | `route_resolved` | observation stub; the 404 body is synthesized separately | + +## Recipes + +Representative examples from [`examples/`](../../examples/) and [`test/integ/`](../../test/integ/): + +- **Per-route auth** — `examples/per_route_auth.cpp`: a `before_handler` hook on `/private` reads `ctx.request->get_user()/get_pass()`, constant-time compares, and returns `pass()` or `respond_with(http_response::unauthorized("Basic","private-realm","Unauthorized"))`. Sibling `/public` is untouched. +- **Early 413 on upload** — `examples/early_413.cpp`: a `request_received` hook checks `Content-Length` against a cap and returns `respond_with(413)` so the body is never read and the handler never runs. +- **Common Log Format access log** — `examples/clf_access_log.cpp`: a `response_sent` hook emits a CLF line from `ctx.status` / `ctx.bytes_queued` / `ctx.elapsed` (control-char sanitized). The modern replacement for v1 `log_access`. +- **Denied-IP logging** — `examples/denied_ip_log.cpp`: an `accept_decision` hook logs each rejection (`ctx.accepted==false`, `ctx.reason`). Observation-only — throwing does not flip the verdict. +- **Replace the response after the handler** — `test/integ/hooks_after_handler_replaces_response.cpp`: an `after_handler` hook returns `respond_with(r2)`; the resource's reply is discarded. +- More coverage: `hooks_body_chunk_*`, `hooks_handler_exception_chain.cpp`, `hooks_response_sent_carries_status_bytes_timing.cpp`, `hooks_connection_lifecycle.cpp`, `hooks_per_route_*`. + +--- +*See also: [request-flow](request-flow.md) (exactly where each phase fires) · [errors](errors.md) (the `handler_exception` phase and the 500 fallback).* diff --git a/docs/architecture/request-flow.html b/docs/architecture/request-flow.html new file mode 100644 index 000000000..7953d39bd --- /dev/null +++ b/docs/architecture/request-flow.html @@ -0,0 +1,455 @@ + + + + + + + + +libhttpserver v2 — request lifecycle & routing + + +
+
+

libhttpserver v2.0 · request lifecycle

+

How a request flows — from the MHD callback to the wire

+

One HTTP request is not one function call. libmicrohttpd drives the exchange through a fixed sequence of C-ABI callbacks (the purple webserver_impl trampolines), each forwarding into a behavior service. This traces that journey end-to-end: the callback spine, the body-accumulation loop, the WebSocket branch, the four-tier route resolution, and where each of the 11 hook phases fires. Companion to the class & filesystem map; same colour language.

+ +
+
acceptpolicy_callbackIP ACL · per connection
+
→
+
allocateconnection_notify · uri_logarena + con_cls
+
→
+
driveanswer_to_connectioncalled 1..N times
+
→
+
finishrequest_completedfire + free
+
+ +
+ MHD C-ABI trampoline + behavior service + state collaborator + domain / value object + + hook phase fires + branch / short-circuit +
+
+ + +
+
01

The callback spine — a normal request, top to bottom

+

Each band is one libmicrohttpd callback (all static members of webserver_impl, defined in webserver_callbacks.cpp). The trampoline marshals the C ABI and delegates to a service. A GET runs straight down; ◈ hook pills mark where a phase fires; dashed chips mark branch points. The shared blackboard threaded through every band is connection_context (*con_cls).

+ + +
+
policy_callbackMHD adapteronce · per TCP connection · before accept
+

Only runs when ip_access_control_enabled. Decides whether to accept the peer at all.

+
+
1
+
ip_access_control::classifybuild ip_representation(addr) → membership{denied, allowed} under shared locks
+
2
+
classify_decisionapply default_policy → (accepted, reason)
+
DENY → return MHD_NO, connection refused
+
3
+
accept_decisionfired via hooks_dispatch_.fire_accept_decision → return MHD_YES
+
+
+
▼
+ + +
+
connection_notifyMHD adapterSTARTED · per connection
+
+
4
+
new connection_stateper-connection PMR arena (8 KB inline buffer) → stored in MHD socket_context; copies max_args_count/bytes
+
5
+
connection_openedfire_connection_opened(ctx)
+
+
+
▼
+ + +
+
uri_logMHD adapteronce · per request
+
+
6
+
make_unique<connection_context>stashes complete_uri; the pointer becomes MHD's *con_cls, handed back to every later callback for this request
+
+
+
▼
+ + +
+
answer_to_connectionMHD adapter → request_pipelinefirst call · request == nullptr
+

First invocation for the request: parse the head, resolve the verb, build the http_request.

+
+
7
+
answer_to_connectionstamp start_time, ws=parent; base_unescaper; canonicalize: standardized_url = normalize_path(standardize_url(url)) — the single security-critical dot-segment collapse
+
8
+
resolve_method_callbackverb → callback = &http_resource::render_get, method_enum, sets has_body
+
unknown verb → callback=nullptr, 405 path
+
9
+
request_pipeline::requests_answer_first_stepbuild http_request in the connection arena (pick_resource)
+
10
+
request_receivedhooks_.fire_request_receivedrespond_with → set response, skip_handler=true
+
11
+
has_body?GET / no body → return MHD_YES, no post-processor. POST / PUT → set content_size_limit, create MHD_PostProcessor for form types → enters the loop below
+
+ + +
+ ⟳ body-accumulation loop · POST / PUT only +

MHD calls answer_to_connection repeatedly, each with a non-empty chunk, routing to request_pipeline::requests_answer_second_step. Loops until MHD delivers a zero-size chunk.

+
+
a
+
body_chunkfired per chunk (body_bytes_seen offset)respond_with → skip_handler, drain
+
b
+
request_pipelineaccumulate: grow_content (raw / put_processed_data_to_content) or MHD_post_process → drives post_iterator
+
c
+
post_iteratorper form field →upload_pipeline::iterate_filememory → set_arg_flat; disk → manage_upload_stream + write into file_info
+
+

Each iteration ends with *upload_data_size = 0 to signal the chunk consumed. No handler runs yet.

+
+
+
▼
+ + +
+
answer_to_connectionMHD adapter → request_pipelineterminal call · *upload_data_size == 0
+
+
12
+
requests_answer_second_step → complete_requestset_path(standardized_url), set_method, set_version → hand off to the dispatcher
+
+
+
▼
+ + +
+
request_dispatcher::finalize_answerbehavior serviceroute · auth · dispatch
+

The heart of the exchange: try to upgrade, resolve the route, run the gate hooks, invoke the handler, catch what it throws.

+
+
13
+
try_ws_upgrade→ websocket_upgrader::try_handleUpgrade: websocket → 101, bypass everything below ↘ (see §3)
+
14
+
skip_handler? → jump straight to materialize (step 20)
+
15
+
resolve_resource_for_request→ route_table::lookup_v2 (see §2); replay captured_params via set_arg
+
not found → errors_.not_found_page · 404
+
16
+
route_resolvedgated; builds route_descriptor{template, methods, is_prefix}
+
17
+
before_handlerserver + per-routeauth & 405-alias hooks run hererespond_with → skip handler
+
18
+
dispatch_resource_handleris_allowed(method_enum) →no → 405 + Allow:
+
(*hrm.*callback)(*request) — pointer-to-member dispatch → your render_get(...) (or lambda_resource::invoke_)
+
handler_exceptionserver + per-routethrew & unhandled → internal_error_page · 500
+
19
+
after_handlerserver + per-routea hook may replace the response
+
+
+
▼
+ + +
+
response_materializer::materialize_and_queue_responsebehavior servicehttp_response → the wire
+
+
20
+
materialize_responseresponse_body::materialize() — polymorphic per body_kind → MHD_Response* (empty · string · file · iovec · pipe · deferred · digest); null/throw → 404/500 fallback
+
21
+
decorate_mhd_responsewalk headers / footers / parsed cookies → MHD_add_response_header
+
22
+
queue_response_dispatching_kind◀ THE SEND — MHD_queue_response (or MHD_queue_auth_required_response3 for a digest challenge)
+
23
+
response_sentserver + per-route + log_access aliasafter queue, before destroy; computes bytes_queued + elapsed
+
24
+
MHD_destroy_responsereleases the caller ref; MHD keeps streaming bodies alive
+
+
+
▼
+ + +
+
request_completedMHD adapterMHD completion callback
+
+
25
+
request_completedserver + per-route via resource_weak_
+
26
+
delete connection_contextruns ~http_request + arena destructors
+
27
+
connection_state::reset_arenarelease + secure_zero for keep-alive reuse
+
+
+
▼
+ + +
+
connection_notifyMHD adapterCLOSED · per connection
+
+
28
+
connection_closedthen delete connection_state
+
+
+
+ + +
+
02

Route resolution — route_table::lookup_v2

+

Step 15 above. Four tiers, cheapest first; the first hit wins and returns a lookup_result{found, tier, entry, captured_params}. Lookup keys off the raw standardized_url string — http_endpoint parsing is a registration-time concern, not per-request. The entry is returned regardless of method so the 405 path still sees it.

+
+
canonpre
+
canonicalize_lookup_path
ensure leading /, strip trailing /; zero-alloc when already canonical
+
Tier 1exact
+
exact_routes_ hit → bypass cache
std::map transparent find under shared_lock — the hottest path, deliberately skips the LRU
+
+
Tier 2cache
+
route_lru_cache.find_by_view hit → promote
LRU over parameter / regex results only; {method, path} key, moves out entry + captured_params
+
+
Tier 3radix
+
param_and_prefix_routes_ · segment_trie hit → radix
walk path segments: literal children, then {name} wildcards (captured), applying any {id|regex} constraint; records best prefix terminus for register_prefix
+
+
Tier 4regex
+
regex_routes_ · linear scan hit → regex
std::regex_match over pre-compiled patterns; last resort
+
installpost
+
route_lru_cache.insert
on any param/regex hit, memoize {entry, captured_params} for next time
+
+

Method → handler. lookup_v2 never filters by method. The verb was already turned into a pointer-to-member (render_get/render_post/…) back in resolve_method_callback (step 8); dispatch_resource_handler then checks http_resource::is_allowed(method_enum) — mismatch yields 405 with the resource's Allow: header.

+
+ + +
+
03

The WebSocket branch — diverges at step 13

+

Before any routing or handler dispatch, finalize_answer offers the request to the upgrader. A genuine upgrade queues 101 Switching Protocols and bypasses routing, all handler hooks, and the response materializer entirely.

+
+ ↘ websocket_upgrader::try_handle → std::optional<MHD_Result> +
+
1
+
try_handleno Upgrade: websocket header → std::nullopt → normal HTTP flow resumes
+
2
+
validate_websocket_handshakecheck Connection: Upgrade, Sec-WebSocket-Version: 13, non-empty keyinvalid → 400
+
3
+
ws_registry::findshared_ptr<websocket_handler> for the URLnone → fall back to normal HTTP
+
4
+
complete_websocket_upgradeSec-WebSocket-Accept + MHD_create_response_for_upgrade → 101 via MHD_queue_response
+
5
+
upgrade_handleron the raw socket: on_open, then a recv() loop → decode frames → on_message / on_binary / on_ping / on_close
+
+
+
+ + +
+
04

The 11 hook phases, in firing order

+

Server-wide hooks live on hook_bus; five phases are also per-resource (resource_hook_table, registered via http_resource::add_hook). hook_dispatcher fires both chains. Every phase is guarded by a relaxed-atomic has_hooks_for check — zero cost when unused. Short-circuit phases can return respond_with(...) to end the request early.

+
+ + + + + + + + + + + + + + + +
PhaseFires inScopeKind
1connection_openedconnection_notify STARTEDserverobserve
2accept_decisionpolicy_callback, after ACL classifyserverobserve
3request_receivedrequests_answer_first_stepservershort-circuit
4body_chunkrequests_answer_second_step · per chunkservershort-circuit
5route_resolvedfinalize_answer, after lookupserverobserve
6before_handlerfinalize_answer, pre-dispatch · authserver + routeshort-circuit
7handler_exceptiondispatch_resource_handler catch armsserver + routeshort-circuit
8after_handlerfinalize_answer, post-handlerserver + routereplace resp
9response_sentmaterialize_and_queue, after queueserver + route · log_access aliasobserve
10request_completedrequest_completed callbackserver + routeobserve
11connection_closedconnection_notify CLOSEDserverobserve
+
+
+ +
+

Reading the flow. Purple bands are the fixed libmicrohttpd callback sequence — webserver_impl's static C-ABI trampolines, the only code touching MHD's ABI. Everything real happens in the teal behavior services they delegate to. The three branch axes that reshape the path: has_body (whether the body loop runs), skip_handler (a request_received/body_chunk short-circuit that skips routing straight to materialize), and the WebSocket upgrade (which bypasses the HTTP tail entirely). One request drives answer_to_connection 1..N times — once for a bodyless GET, many times while a POST body streams in — but resolves to exactly one finalize_answer. Companion to the class & filesystem map; both share the state / behavior / adapter colour language.

+
+
+ + + diff --git a/docs/architecture/request-flow.md b/docs/architecture/request-flow.md new file mode 100644 index 000000000..efe307e9e --- /dev/null +++ b/docs/architecture/request-flow.md @@ -0,0 +1,94 @@ +# Request lifecycle & routing flow + +> How an HTTP request travels from the libmicrohttpd callback to the wire, for libhttpserver v2.0. +> Quick-view below; the full step-by-step page (28 ordered steps, the body loop, the WebSocket branch, the hook rail) is **[`request-flow.html`](request-flow.html)** (open in a browser). + +One HTTP request is not one function call. libmicrohttpd drives the exchange through a fixed sequence of C-ABI callbacks (static `webserver_impl` trampolines in `webserver_callbacks.cpp`), each forwarding into a behavior service. `answer_to_connection` is called **1..N times** — once for a bodyless `GET`, many times while a `POST` body streams in — but resolves to exactly one `finalize_answer`. + +## The callback spine + +```mermaid +sequenceDiagram + autonumber + participant MHD as libmicrohttpd + participant AD as webserver_impl trampolines + participant PL as request_pipeline + participant DP as request_dispatcher + participant RT as route_table + participant RM as response_materializer + + MHD->>AD: policy_callback · IP ACL + Note over AD: ip_access_control.classify → accept? · ◈ accept_decision + MHD->>AD: connection_notify STARTED + Note over AD: new connection_state (arena) · ◈ connection_opened + MHD->>AD: uri_log + Note over AD: new connection_context → *con_cls + + MHD->>AD: answer_to_connection · first · request==nullptr + Note over AD: canonicalize URL · resolve_method_callback + AD->>PL: requests_answer_first_step + Note over PL: build http_request · ◈ request_received short-circuit + alt has body · POST or PUT + loop until zero-size chunk + MHD->>AD: answer_to_connection · body chunk + AD->>PL: requests_answer_second_step + Note over PL: ◈ body_chunk · grow_content / post_iterator → upload_pipeline + end + end + + MHD->>AD: answer_to_connection · terminal · size==0 + AD->>PL: requests_answer_second_step → complete_request + PL->>DP: finalize_answer + Note over DP: try_ws_upgrade → 101 branch bypasses the rest + DP->>RT: lookup_v2 · exact→cache→radix→regex + RT-->>DP: entry + captured_params · else 404 + Note over DP: ◈ route_resolved · ◈ before_handler auth · is_allowed → 405 + Note over DP: pointer-to-member → render_get · catch → ◈ handler_exception → 500 + Note over DP: ◈ after_handler may replace response + DP->>RM: materialize_and_queue_response + Note over RM: response_body.materialize → decorate → MHD_queue_response ◀ SEND · ◈ response_sent + MHD->>AD: request_completed + Note over AD: ◈ request_completed · delete connection_context · reset_arena + MHD->>AD: connection_notify CLOSED + Note over AD: ◈ connection_closed · delete connection_state +``` + +## Four-tier route resolution + +`route_table::lookup_v2` — cheapest tier first, first hit wins: + +```mermaid +flowchart LR + C["canonicalize path"] --> T1{"Tier 1
exact_routes_ map"} + T1 -- hit --> H["entry + captured_params"] + T1 -- miss --> T2{"Tier 2
LRU cache"} + T2 -- hit --> H + T2 -- miss --> T3{"Tier 3
segment_trie radix"} + T3 -- hit --> INS["cache install"] --> H + T3 -- miss --> T4{"Tier 4
regex scan"} + T4 -- hit --> INS + T4 -- miss --> NF["404 not found"] +``` + +The entry is returned **regardless of method** so the 405 path still sees it. Method → handler is chosen separately: the verb became a pointer-to-member (`render_get`/`render_post`/…) in `resolve_method_callback`, and `dispatch_resource_handler` checks `http_resource::is_allowed(method_enum)` → mismatch yields **405** + the resource's `Allow:` header. + +## Where the hook phases fire + +Server-wide hooks live on `hook_bus`; the five post-route-resolution phases are also **per-resource** (`resource_hook_table`, via `http_resource::add_hook`). Every phase is guarded by a relaxed-atomic `has_hooks_for` check — zero cost when unused. The full context-struct catalog and usage recipes are in **[hooks.md](hooks.md)**. + +| # | Phase | Fires in | Scope | Kind | +|---|---|---|---|---| +| 1 | `connection_opened` | `connection_notify` STARTED | server | observe | +| 2 | `accept_decision` | `policy_callback` | server | observe | +| 3 | `request_received` | `requests_answer_first_step` | server | short-circuit | +| 4 | `body_chunk` | `requests_answer_second_step` (per chunk) | server | short-circuit | +| 5 | `route_resolved` | `finalize_answer` (after lookup) | server | observe | +| 6 | `before_handler` | `finalize_answer` (pre-dispatch · **auth**) | server + route | short-circuit | +| 7 | `handler_exception` | `dispatch_resource_handler` catch | server + route | short-circuit | +| 8 | `after_handler` | `finalize_answer` (post-handler) | server + route | replace resp | +| 9 | `response_sent` | `materialize_and_queue` (after queue) | server + route · log_access alias | observe | +| 10 | `request_completed` | `request_completed` callback | server + route | observe | +| 11 | `connection_closed` | `connection_notify` CLOSED | server | observe | + +--- +*See also: [class map](class-map.md) (the classes on each lane) · [hooks.md](hooks.md) (the hook cookbook) · [errors.md](errors.md) (the 404/405/500 origins).* diff --git a/docs/architecture/threading.md b/docs/architecture/threading.md new file mode 100644 index 000000000..ba28423eb --- /dev/null +++ b/docs/architecture/threading.md @@ -0,0 +1,96 @@ +# Threading & locking model (DR-008) + +> Concurrency contract, mutex inventory, and lock ordering for libhttpserver v2.0. +> Decision record: [`specs/architecture/11-decisions/DR-008.md`](../../specs/architecture/11-decisions/DR-008.md) · cross-cutting §5.1 / §5.6: [`05-cross-cutting.md`](../../specs/architecture/05-cross-cutting.md). + +## The contract + +DR-008 chose **Option 1 — internally synchronized, fully re-entrant**. Every `webserver` public method is thread-safe and may be called *from inside a handler*, including live route (un)registration, IP allow/deny, and hook registration. + +- **Handlers run concurrently** on MHD worker threads — the same lambda / `http_resource` / hook is invoked from many threads at once. **Your own state must be user-synchronized.** +- `http_request` is **single-threaded per request** — owned by the servicing worker, must not outlive the handler return or be shared across threads (UB). +- `http_response` is a **move-only value type**; returning it transfers ownership. +- **The one exception to "call anything, anytime":** `stop()`, `stop_and_wait()`, and `~webserver()` **must not be called from a handler thread** — they join MHD's workers (the very thread calling them) and will deadlock, or abort with *"Failed to join a thread."* on some MHD builds. Call them from the thread that owns the server. +- **Cache-staleness caveat:** the route LRU cache is consulted *outside* `route_table_mutex_`, so a request that hit the cache can still be served by a just-unregistered resource. This is not use-after-free (the cache holds a `shared_ptr`); the single response is merely logically stale. + +Verified by `test/integ/threadsafety_stress.cpp` under the **TSan** CI lane (16 curl clients × 60 s mutating routes/IPs from handler threads), plus the opt-in `stop_from_handler_deadlocks_as_documented` negative test. + +## Two threading models + +```mermaid +flowchart TB + subgraph internal["Internal threading · default"] + direction TB + M1["MHD polling / thread-per-connection"] --> W1["worker threads"] + W1 --> H1["handlers · hooks · log callbacks
run concurrently"] + end + subgraph external["External event loop"] + direction TB + L1["your select / epoll loop"] --> R1["run · run_wait · get_fdset · add_connection"] + R1 --> H2["handlers run on YOUR loop thread"] + end +``` + +- **Internal** (default; `start_method`, `max_threads`): MHD owns the threads. `max_threads != 0` adds `MHD_OPTION_THREAD_POOL_SIZE`. All user callbacks — render methods, hooks, `log_access`/`log_error`, error handlers, PSK/SNI/ALPN, `file_cleanup` — run on MHD workers, concurrently. +- **External**: start without internal threads and drive with `run()` / `run_wait(ms)` / `get_fdset()` + `get_timeout()` / `add_connection()`. Dispatch runs on **your** loop thread; `run()`/`run_wait()` catch handler exceptions internally. +- **`daemon_lifecycle` start/stop handshake** uses POSIX primitives (`mutexwait`/`mutexcond`, `pthread_*`) rather than `std::` because MHD's start path expects them. On a blocking `start()` the caller `pthread_cond_wait`s until `stop()` flips `running=false` and signals. + +## Mutex inventory + +Each state collaborator owns its own mutex(es); **no lock nests with another across a call-out** (see below). `shared_mutex` gives many-reader / one-writer. + +| Owner | Member | Type | Guards | Read / write | +|---|---|---|---|---| +| `route_table` | `route_table_mutex_` | `shared_mutex` | the 3 route tiers (`exact_routes_`, `param_and_prefix_routes_` trie, `regex_routes_`) | lookup = `shared`; (un)register = `unique` via `lock_for_write()` | +| `route_cache` | `mutex_` | `mutex` | LRU list + index (every touch splices → a write) | `lock_guard` on every access | +| `hook_bus` | `hook_table_mutex_` | `shared_mutex` | the 11 server-wide phase vectors | add/remove = `unique`; fire = `shared` (snapshot + release) | +| `hook_bus` | `any_hooks_[11]`, `next_slot_id_` | `atomic[]`, `atomic` | hot-path skip gate; slot allocator | relaxed load on dispatch; set under `unique` | +| `resource_hook_table` | `hook_table_mutex_` | `shared_mutex` | the 5 per-resource phase vectors | append/remove = `unique`; fire = `shared` | +| `ip_access_control` | `deny_list_mutex_` | `shared_mutex` | `deny_list_` | mutate = `unique`; `classify` = `shared` | +| `ip_access_control` | `allow_list_mutex_` | `shared_mutex` | `allow_list_` | mutate = `unique`; `classify` = `shared` | +| `ws_registry` | `mutex_` | `shared_mutex` | `handlers_` map | (un)register = `unique`; `find`/`empty` = `shared` | +| `daemon_lifecycle` | `mutexwait` / `mutexcond` | `pthread_mutex_t` / `pthread_cond_t` | blocking start/stop handshake | raw `pthread_*` | +| `daemon_lifecycle` | `daemon`, `running` | `atomic`, `atomic` | published handle + ephemeral port; started flag | release/acquire; lock-free reads | +| `http_resource` | `cached_allow_mutex_` | `mutable shared_mutex` | **only** the lazy `Allow:`-header cache (`cached_allow_header_/mask_/valid_`) — *not* `methods_allowed_` | warm read = `shared`; fill = `unique` (double-checked) | +| `http_resource` | `hook_table_` | `shared_ptr` via `std::atomic_*` | lock-free publication of the per-resource hook table | `atomic_load(acquire)` / CAS(acq_rel) | +| `webserver_impl` | `sni_credentials_mutex` | `mutable shared_mutex` *(HAVE_GNUTLS + cert-callback)* | `sni_credentials_cache` | read = `shared`; fill = `unique` | + +`http_request_impl` has **no mutex** — single-threaded per request by contract. `segment_trie` has none — it is covered by the owning `route_table_mutex_`. + +## The "no two locks co-held" invariant + +Two facts hold together: + +1. **The clusters are disjoint.** IP-ACL, `ws_registry`, and SNI mutexes are never taken with any other cluster's lock. The single deliberate intra-class co-hold is `ip_access_control::classify()`, which takes **both** `deny_list_mutex_` and `allow_list_mutex_` as `shared_lock`, always **deny-then-allow**, for a consistent snapshot — never with any third mutex held. +2. **The two nesting families have a documented order — but never hold two across a call-out.** Each site takes a lock, *snapshots* the data, *releases*, then acts. This release-before-acquire is exactly what makes `add_hook` / `hook_handle::remove` safe to call from inside a running hook. + +```mermaid +flowchart LR + RT["route_table_mutex_"] -->|then| RH["resource hook_table_mutex_"] -->|then| SH["server hook_table_mutex_"] +``` + +- **Route family:** `route_table_mutex_` before `route_cache::mutex_` — but lookup releases the table lock *then* promotes into the cache; registration releases *then* clears it. +- **Hook family:** `route_table_mutex_` → resource `hook_table_mutex_` → server `hook_table_mutex_`. Each firing site copies the phase vector under a `shared_lock`, releases, then invokes user code with no lock held. + +TSan-validated by `threadsafety_stress.cpp` and the `check-route-table-concurrency` lane. + +## Public-API concurrency + +| Safe from any thread, incl. inside a handler | Must be called only from the owning thread | +|---|---| +| `register_path` · `register_prefix` · `on_get`/`on_post`/… · `route` | `stop()` | +| `unregister_path` · `unregister_prefix` · `unregister_resource` | `stop_and_wait()` | +| `register_ws_resource` · `unregister_ws_resource` | `~webserver()` | +| `deny_ip` · `remove_denied_ip` · `allow_ip` · `remove_allowed_ip` | | +| `add_hook` · `http_resource::add_hook` · `hook_handle::remove` | | + +A duplicate registration from a handler throws `std::invalid_argument` — no data race. Lifecycle atomics: `running` lets `is_running()`/`stop()` read lock-free while start/stop writes under the mutex; `daemon` (release/acquire) publishes the handle and ephemeral bind port so `get_bound_port()` is safe on another thread during a blocking `start()`. + +## Helgrind lane + +Policy (mirrors DR-008): **any race Helgrind reports inside libhttpserver's own locking is a real bug — fix the source, never suppress it.** Only narrow, per-symbol, third-party or provably-benign entries are allowed in [`test/valgrind-helgrind.supp`](../../test/valgrind-helgrind.supp) — never a `...` wildcard spanning a library locking frame. + +The false positives come from MHD signalling its workers over an **internal ITC pipe** (not a pthread primitive), which Helgrind's `--history-level=approx` cannot see as a happens-before — so it flags benign races on MHD worker frames, immutable-after-start config reads, and state published via the HTTP round-trip. **DR-014 split dispatch into its own TUs, changing inlining and lifting the write frames**, which forced re-anchoring those suppressions on the **test-fixture handler symbol** (e.g. `*print_request_resource*render_get*`) rather than any library frame — so a genuine library race stays unsuppressed. `scripts/check-dr008-lanes.sh` is a structural gate asserting the two DR-008 CI steps (`check-route-table-concurrency` on TSan, `check-stop-from-handler` on the baseline lane) are never silently dropped. TSan (precise) is green across all of these. + +--- +*See also: [request-flow](request-flow.md) (which callbacks run on which thread) · [class map](class-map.md) (the collaborators that own these mutexes).* diff --git a/docs/debug-env-vars.md b/docs/debug-env-vars.md new file mode 100644 index 000000000..ca30def56 --- /dev/null +++ b/docs/debug-env-vars.md @@ -0,0 +1,102 @@ +# Debug-only environment variables + +This document inventories environment variables that the libhttpserver +runtime reads. **None of these are meant to be set in production.** +All are silent unless explicitly enabled; the library emits a one-shot +`SECURITY WARNING` to stderr (and to the user's `log_error` callback, +when wired) the first time `webserver::start()` observes a set +variable. + +--- + +## `LIBHTTPSERVER_DEBUG_DUMP_REQUEST_BODY` + +| Field | Value | +| --- | --- | +| Introduced | TASK-074 (June 2026) | +| Replaces | The prior compile-time `#ifdef DEBUG` guard in `src/detail/webserver_body_pipeline.cpp` | +| Read by | `httpserver::detail::debug_dump_request_body_opted_in()` (cached in a function-local static) | +| Independent of | Compile-time `DEBUG` / `NDEBUG`. The runtime check is the only gate. | + +### Effect + +When set to any non-empty, non-`"0"` value, every inbound request body +chunk is written verbatim to `stdout`, prefixed by `Writing content: `, +from the body pipeline in `src/detail/webserver_body_pipeline.cpp`. + +### Risk + +Raw request bodies routinely contain: +- Basic-auth credentials (when sent in the body rather than the header), +- form-posted passwords (`application/x-www-form-urlencoded` with a + `password=` field), +- session cookies, +- CSRF tokens, +- PII (email addresses, names, IDs, addresses). + +The `http_request::operator<<` redaction policy introduced in TASK-057 +does **not** cover this code path -- bytes are written verbatim. Treat +any process where this variable is set as a credential-leaking +surface (CWE-532: Insertion of Sensitive Information into Log File). + +### Startup signal + +The library emits a single `SECURITY WARNING` line to stderr at the +first `webserver::start()` call after the variable is observed. The +exact line is: + +``` +[libhttpserver] SECURITY WARNING: LIBHTTPSERVER_DEBUG_DUMP_REQUEST_BODY is set. Raw request bodies will be written to stdout, including credentials, session cookies, and PII. Unset the variable for production deployments. See docs/debug-env-vars.md. +``` + +The same line is forwarded to the owning webserver's `log_error` +callback when one is wired via `create_webserver().log_error(...)`. The +forwarding is best-effort: an exception from the callback is swallowed +so a misconfigured logger cannot abort daemon startup. + +Multiple `webserver` instances constructed in the same process share a +process-wide print-once flag, so the warning is emitted exactly once +per process. + +**stdout/stderr are independent streams.** The request-body dump goes to +`stdout`; the `SECURITY WARNING` above goes to `stderr`. Process +supervisors, container log aggregators, and the systemd journal may +capture these two streams separately (different files, different log +sinks, or different retention policies) rather than interleaving them +into one ordered log. If only `stdout` is retained/shipped, the leaked +credential/PII lines can end up with no accompanying warning context; +if only `stderr` is retained, the warning fires with no visible dump to +correlate it against. Operators enabling this variable must ensure both +streams are captured together (e.g. `2>&1` redirection, or a supervisor +configured to merge/correlate them) so the warning and the dump it +describes stay associated. + +### How to disable + +```sh +unset LIBHTTPSERVER_DEBUG_DUMP_REQUEST_BODY +``` + +then restart the process. The value is cached in a function-local +static on first request, so a mid-process `unsetenv()` has no effect. + +### Spot-check: release build with `-DDEBUG` accidentally set + +The runtime check is the only gate; the env var must be explicitly set +for the dump to fire, regardless of the compile-time `DEBUG` / +`NDEBUG` posture. A release binary built with `-DDEBUG` accidentally +defined therefore still does not leak credentials/PII unless the +operator opted in. + +--- + +## Adding new debug env vars + +When introducing a new `LIBHTTPSERVER_*` debug environment variable: + +1. Read it once via a function-local static (cached, race-free C++ + memory model semantics). +2. Add a one-shot stderr warning in `webserver::start()` naming the + risk explicitly. +3. Document the variable, its risk, and its startup signal here. +4. Cross-reference the entry from `specs/architecture/10-observability.md`. diff --git a/doxyconfig.in b/doxyconfig.in index 887c682d5..5956143b2 100644 --- a/doxyconfig.in +++ b/doxyconfig.in @@ -179,7 +179,7 @@ TAB_SIZE = 8 # will result in a user-defined paragraph with heading "Side Effects:". # You can put \n's in the value part of an alias to insert newlines. -ALIASES = +ALIASES = "security=\par Security:^^" # Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C # sources only. Doxygen will then generate output that is more tailored for C. @@ -610,7 +610,7 @@ RECURSIVE = NO # excluded from the INPUT source files. This way you can easily exclude a # subdirectory from a directory tree whose root is specified with the INPUT tag. -EXCLUDE = test/* +EXCLUDE = test/* # The EXCLUDE_SYMLINKS tag can be used select whether or not files or # directories that are symbolic links (a Unix filesystem feature) are excluded @@ -1348,7 +1348,7 @@ SEARCH_INCLUDES = YES # contain include files that are not input files but should be processed by # the preprocessor. -INCLUDE_PATH = +INCLUDE_PATH = $(SRCDIR)/src # You can use the INCLUDE_FILE_PATTERNS tag to specify one or more wildcard # patterns (like *.h and *.hpp) to filter out the header-files in the @@ -1479,7 +1479,7 @@ DOT_NUM_THREADS = 0 # DOTFONTPATH environment variable or by setting DOT_FONTPATH to the directory # containing the font. -DOT_FONTNAME = FreeSans.ttf +DOT_FONTNAME = Helvetica # The DOT_FONTSIZE tag can be used to set the size of the font of dot graphs. # The default size is 10pt. diff --git a/examples/Makefile.am b/examples/Makefile.am index e0c44ac63..d72ab082e 100644 --- a/examples/Makefile.am +++ b/examples/Makefile.am @@ -19,8 +19,75 @@ LDADD = $(top_builddir)/src/libhttpserver.la AM_CPPFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/src/httpserver/ METASOURCES = AUTO -noinst_PROGRAMS = hello_world service +noinst_PROGRAMS = hello_world shared_state service custom_error allowing_disallowing_methods handlers hello_with_get_arg args_processing setting_headers custom_access_log clf_access_log minimal_https minimal_file_response minimal_deferred url_registration minimal_ip_access_control denied_ip_log early_413 per_route_auth benchmark_select benchmark_threads benchmark_nodelay deferred_with_accumulator file_upload file_upload_with_callback empty_response_example iovec_response_example pipe_response_example daemon_info external_event_loop turbo_mode binary_buffer_response hello_world_SOURCES = hello_world.cpp +shared_state_SOURCES = shared_state.cpp service_SOURCES = service.cpp +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 +# TASK-050: CLF-format access logger written as a response_sent hook. +# Demonstrates the resolution of issues #281 and #69 -- with the +# structured response_sent_ctx (status / bytes_queued / elapsed), users +# can write a real CLF / time-taken log line in user code, without a +# library change. +clf_access_log_SOURCES = clf_access_log.cpp +minimal_https_SOURCES = minimal_https.cpp +minimal_file_response_SOURCES = minimal_file_response.cpp +minimal_deferred_SOURCES = minimal_deferred.cpp +deferred_with_accumulator_SOURCES = deferred_with_accumulator.cpp +url_registration_SOURCES = url_registration.cpp +minimal_ip_access_control_SOURCES = minimal_ip_access_control.cpp +denied_ip_log_SOURCES = denied_ip_log.cpp +early_413_SOURCES = early_413.cpp +# TASK-052: per-route auth via http_resource::add_hook(before_handler). +# Demonstrates that a hook registered on one resource fires only when +# that resource is dispatched -- a private route can require credentials +# without touching sibling routes (DR-012). +per_route_auth_SOURCES = per_route_auth.cpp +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 +empty_response_example_SOURCES = empty_response_example.cpp +iovec_response_example_SOURCES = iovec_response_example.cpp +pipe_response_example_SOURCES = pipe_response_example.cpp +daemon_info_SOURCES = daemon_info.cpp +external_event_loop_SOURCES = external_event_loop.cpp +turbo_mode_SOURCES = turbo_mode.cpp +binary_buffer_response_SOURCES = binary_buffer_response.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 +# client_cert_auth demonstrates mutual-TLS / client-certificate access control. +# Its handler consumes only the public http_request cert accessors (which are +# always on the surface), but the example is meaningful only when TLS support +# is compiled in, so it is built on the GnuTLS lane where an HTTPS server can +# actually be started. +noinst_PROGRAMS += minimal_https_psk client_cert_auth +minimal_https_psk_SOURCES = minimal_https_psk.cpp +client_cert_auth_SOURCES = client_cert_auth.cpp +endif + +if HAVE_DAUTH +noinst_PROGRAMS += digest_authentication +digest_authentication_SOURCES = digest_authentication.cpp +endif + +if HAVE_WEBSOCKET +noinst_PROGRAMS += websocket_echo +websocket_echo_SOURCES = websocket_echo.cpp +websocket_echo_LDADD = $(LDADD) -lmicrohttpd_ws +endif diff --git a/examples/README.md b/examples/README.md index d2faca335..0eb58e46f 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,58 +1,143 @@ -Example Programs -================ - -hello_world.cpp - a very simple example of using libhttpserver to - create a Rest server capable of receiving and processing - HTTP requests. The server will be listening on port - 8080. - - -service.cpp - an example using more of the libhttpserver API. - This creates a Rest server capable of running with - HTTP or HTTPS (provided that libhttpserver and - libmicrohttpd have been compiled with SSL support. - - The server can be configured via command line - arguments: +libhttpserver Examples +====================== - -p - port number to listen on (default 8080) - -s - enable HTTPS - -k - server key filename (default "key.pem") - -c - server certificate filename (default "cert.pem") +Every example is a standalone program built from a single `.cpp`. To +build the suite locally: + + ./bootstrap && mkdir build && cd build && ../configure && make + +Each binary lands in `build/examples/`. To verify that an example also +compiles against an *installed* libhttpserver (the consumer view), run +`scripts/verify-installed-examples.sh` after `make install`. To enforce +the structural invariants on `hello_world.cpp` and `shared_state.cpp` +(LOC budget, lambda-only form, mutex usage), run +`scripts/check-examples.sh`. + +Start here +---------- + +* `hello_world.cpp` — ten lines, no subclassing, no raw pointers. The + canonical "hello world" demo and the PRD §3.4 acceptance fixture for + the v2.0 lambda idiom. +* `shared_state.cpp` — the canonical example of when the class form is + the right shape: an `http_resource` subclass that shares a `std::mutex`- + guarded counter between `render_get` and `render_post`. + +HTTP basics +----------- + +* `setting_headers.cpp` — attach response headers fluently with + `with_header`. +* `hello_with_get_arg.cpp` — read a query-string parameter from + `http_request::get_arg`. +* `args_processing.cpp` — iterate every form/query argument. +* `custom_error.cpp` — install custom `not_found_handler` and + `method_not_allowed_handler` and let `set_allowing` restrict methods + on a resource. +* `allowing_disallowing_methods.cpp` — narrow a resource to a single + HTTP method via the allow mask. +* `url_registration.cpp` — exact paths, prefix matches, regex paths, + and parametric segments (with optional per-segment regex). +* `handlers.cpp` — register distinct lambdas for GET and POST on the + same path; the dispatcher composes them. + +Response shapes +--------------- + +* `minimal_file_response.cpp` — stream a file from disk. +* `binary_buffer_response.cpp` — return a binary buffer (e.g. a PNG). +* `iovec_response_example.cpp` — gather a response body from multiple + borrowed buffers without copying. +* `pipe_response_example.cpp` — stream from a pipe filled by a + background thread. +* `empty_response_example.cpp` — bodyless responses for DELETE / HEAD. +* `minimal_deferred.cpp` — deferred response: the body is produced + asynchronously by a callback. +* `deferred_with_accumulator.cpp` — deferred response that mutates + shared state across calls. + +TLS and authentication +---------------------- + +* `minimal_https.cpp` — enable TLS with PEM key/cert files. +* `minimal_https_psk.cpp` — TLS with pre-shared keys. +* `basic_authentication.cpp` — per-request HTTP Basic auth inside the + handler. +* `centralized_authentication.cpp` — server-wide `auth_handler` and + `auth_skip_paths`. +* `digest_authentication.cpp` — per-request HTTP Digest auth via + `http_request::check_digest_auth`. +* `client_cert_auth.cpp` — mutual TLS with X.509 client certificates. + Ships as a documentation artifact; not wired into `noinst_PROGRAMS`. + +Lifecycle hooks +--------------- + +The hook bus (DR-012, §4.10) lets user code observe and short-circuit +every interesting point in the request lifecycle. The four examples +below demonstrate the user-visible resolution of issues #332, #273, +#281, #69, and the per-route hook contract introduced in TASK-051. + +* `denied_ip_log.cpp` — a single `accept_decision` hook logs every + denied-IP rejection to stderr. Resolves issue #332. +* `early_413.cpp` — a single `request_received` hook returns + `respond_with(http_response::empty().with_status(413))` when + `Content-Length` exceeds the configured cap. The request body never + crosses the I/O boundary. Resolves issue #273. +* `clf_access_log.cpp` — a single `response_sent` hook formats a + Common Log Format line using the structured context fields + (`status`, `bytes_queued`, `elapsed`). Resolves issues #281 and #69. +* `per_route_auth.cpp` — `http_resource::add_hook(before_handler, ...)` + guards a single route with HTTP Basic auth without touching sibling + routes. Demonstrates per-route hook scoping (DR-012). + +Operations +---------- + +* `daemon_info.cpp` — introspect the running daemon (bound port, + listen FD, active connections). +* `external_event_loop.cpp` — drive `EXTERNAL_SELECT` from the + application's loop via `run_wait`. +* `custom_access_log.cpp` — server-wide access-log callback. +* `minimal_ip_access_control.cpp` — `deny_ip` / `allow_ip` under the + `ACCEPT` and `REJECT` policies. +* `turbo_mode.cpp` — turbo, suppressed Date header, fastopen queue, + listen backlog. +* `service.cpp` — the kitchen-sink reference example: CLI args, + optional TLS, all `render_*` overrides. + +Benchmarks +---------- + +* `benchmark_select.cpp`, `benchmark_threads.cpp`, + `benchmark_nodelay.cpp` — micro-benchmarks. See `test/v1_baseline/` for + v1.x reference numbers. + +WebSockets +---------- + +* `websocket_echo.cpp` — `websocket_handler` subclass registered via + `register_ws_resource` with `std::make_unique`. + +File upload +----------- + +* `file_upload.cpp`, `file_upload_with_callback.cpp` — multipart + upload, GET serves the HTML form and POST processes the parts. Creating Certificates ===================== + Self-signed certificates can be created using OpenSSL using the following steps: - $ openssl genrsa -des3 -passout pass:x -out server.pass.key 2048 - $ openssl rsa -passin pass:x -in server.pass.key -out server.key - $ openssl req -new -key server.key -out server.csr - $ openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt + $ openssl genrsa -des3 -passout pass:x -out server.pass.key 2048 + $ openssl rsa -passin pass:x -in server.pass.key -out server.key + $ openssl req -new -key server.key -out server.csr + $ openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt On the last step when prompted for a challenge password it can be left empty. Thanks to https://devcenter.heroku.com/articles/ssl-certificate-self for these instructions. - -Keystore configuration -====================== -If using a local client such as RestClient -(https://github.com/wiztools/rest-client) for testing the Rest server -then a keystore needs to be established. These commands should be -bundled with your Java installation. - -$ keytool -noprompt -import -keystore /path/to/restclient.store -alias -restclient -file /path/to/server.crt - -The keys in the store can be listed as follows: - -$ keytool -list -v -keystore /path/to/restclient.store - -The client can then be configured to use this keystore. Thanks to -http://rubensgomes.blogspot.com/2012/01/how-to-set-up-restclient-for-ssl.html -for instructions on configuring RestClient. - - - diff --git a/examples/allowing_disallowing_methods.cpp b/examples/allowing_disallowing_methods.cpp new file mode 100644 index 000000000..2830e4c34 --- /dev/null +++ b/examples/allowing_disallowing_methods.cpp @@ -0,0 +1,47 @@ +/* + 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 + +// The class form is required here: disallow_all() and set_allowing() are +// methods on http_resource that control which HTTP methods are accepted. +// Lambda-registered routes (ws.on_get / ws.on_post / etc.) target a single +// method by construction and do not expose these per-resource controls. +// Use the class form whenever you need fine-grained per-instance method ACLs. +class hello_world_resource : public httpserver::http_resource { + public: + httpserver::http_response render(const httpserver::http_request&) override { + return httpserver::http_response::string("Hello, World!"); + } +}; + +int main() { + httpserver::webserver ws{httpserver::create_webserver(8080)}; + + 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 new file mode 100644 index 000000000..812669d33 --- /dev/null +++ b/examples/args_processing.cpp @@ -0,0 +1,95 @@ +/* + This file is part of libhttpserver + Copyright (C) 2011-2025 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 +*/ + +// args_processing.cpp - demonstrates 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 + +#include +#include +#include +#include + +#include + +int main() { + httpserver::webserver ws{httpserver::create_webserver(8080)}; + + ws.on_get("/args", [](const httpserver::http_request& req) { + std::ostringstream response_body; + + response_body << "=== Using get_args() (supports multiple values per key) ===\n\n"; + + // 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 + 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 httpserver::http_response::string(response_body.str()); + }); + + 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/examples/basic_authentication.cpp b/examples/basic_authentication.cpp new file mode 100644 index 000000000..e8d02b302 --- /dev/null +++ b/examples/basic_authentication.cpp @@ -0,0 +1,64 @@ +/* + This file is part of libhttpserver + Copyright (C) 2011-2025 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 +*/ + +// basic_authentication.cpp - per-request HTTP Basic auth check inside a +// lambda handler. For centralized auth that intercepts every request, +// see centralized_authentication.cpp. +// +// SECURITY NOTE: Credentials MUST NOT be hardcoded in source code (CWE-798). +// This example loads BASIC_USER and BASIC_PASS from environment variables. +// Never reflect the password in the response body. +// +// Usage: +// export BASIC_USER=myuser BASIC_PASS=mysecretpassword +// ./basic_authentication +// curl -u myuser:mysecretpassword http://localhost:8080/hello + +#include +#include +#include + +#include + +int main() { + const char* expected_user = std::getenv("BASIC_USER"); + const char* expected_pass = std::getenv("BASIC_PASS"); + if (!expected_user || !expected_pass) { + std::cerr << "basic_authentication: BASIC_USER and BASIC_PASS " + "environment variables must be set.\n"; + return 1; + } + + httpserver::webserver ws{httpserver::create_webserver(8080)}; + + ws.on_get("/hello", [expected_user, expected_pass] + (const httpserver::http_request& req) { + if (req.get_user() != expected_user || req.get_pass() != expected_pass) { + return httpserver::http_response::unauthorized( + "Basic", "test@example.com", "FAIL"); + } + // Only echo the username — never reflect the password back to the client. + return httpserver::http_response::string( + "Hello, " + std::string(req.get_user()) + "!"); + }); + + ws.start(true); + return 0; +} diff --git a/examples/benchmark.cpp b/examples/benchmark.cpp deleted file mode 100755 index 94e4bc26b..000000000 --- a/examples/benchmark.cpp +++ /dev/null @@ -1,68 +0,0 @@ -/* - 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 - -#if defined(CPU_COUNT) && (CPU_COUNT+0) < 2 -#undef CPU_COUNT -#endif -#if !defined(CPU_COUNT) -#define CPU_COUNT 2 -#endif - -#define NUMBER_OF_THREADS CPU_COUNT - -#define PAGE "libmicrohttpd demolibhttpserver demo" - -using namespace httpserver; - -class hello_world_resource : public http_resource { - public: - const http_response render(const http_request&); -}; - -//using the render method you are able to catch each type of request you receive -const http_response hello_world_resource::render(const http_request& req) -{ - //it is possible to send a response initializing an http_string_response - //that reads the content to send in response from a string. - return http_response_builder(PAGE, 200).string_response(); -} - -int main() -{ - //it is possible to create a webserver passing a great number of parameters. - //In this case we are just passing the port and the number of thread running. - webserver ws = create_webserver(8080).start_method(http::http_utils::INTERNAL_SELECT).max_threads(NUMBER_OF_THREADS); - - hello_world_resource hwr; - //this way we are registering the hello_world_resource to answer for the endpoint - //"/hello". The requested method is called (if the request is a GET we call the render_GET - //method. In case that the specific render method is not implemented, the generic "render" - //method is called. - ws.register_resource("/", &hwr, true); - - //This way we are putting the created webserver in listen. We pass true in order to have - //a blocking call; if we want the call to be non-blocking we can just pass false to the - //method. - ws.start(true); - return 0; -} diff --git a/examples/benchmark_nodelay.cpp b/examples/benchmark_nodelay.cpp new file mode 100755 index 000000000..3c4717ea3 --- /dev/null +++ b/examples/benchmark_nodelay.cpp @@ -0,0 +1,53 @@ +/* + 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 + +static constexpr const char* kPath = "/plaintext"; +static constexpr const char* kBody = "Hello, World!"; + +class hello_world_resource : public httpserver::http_resource { + public: + // TASK-036: http_response is a movable value type; build it + // per-request rather than caching a shared instance. + httpserver::http_response render(const httpserver::http_request&) override { + return httpserver::http_response::string(kBody) + .with_header("Server", "libhttpserver"); + } +}; + +int main(int argc, char** argv) { + std::ignore = argc; + + httpserver::webserver ws{httpserver::create_webserver(atoi(argv[1])) + .start_method(httpserver::http::http_utils::INTERNAL_SELECT) + .tcp_nodelay() + .max_threads(atoi(argv[2]))}; + + auto hwr = std::make_shared(); + ws.register_path(kPath, hwr); + + ws.start(true); + + return 0; +} diff --git a/examples/benchmark_select.cpp b/examples/benchmark_select.cpp new file mode 100755 index 000000000..8a8a00e4b --- /dev/null +++ b/examples/benchmark_select.cpp @@ -0,0 +1,52 @@ +/* + 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 + +static constexpr const char* kPath = "/plaintext"; +static constexpr const char* kBody = "Hello, World!"; + +class hello_world_resource : public httpserver::http_resource { + public: + // TASK-036: http_response is a movable value type; build it + // per-request rather than caching a shared instance. + httpserver::http_response render(const httpserver::http_request&) override { + return httpserver::http_response::string(kBody) + .with_header("Server", "libhttpserver"); + } +}; + +int main(int argc, char** argv) { + std::ignore = argc; + + httpserver::webserver ws{httpserver::create_webserver(atoi(argv[1])) + .start_method(httpserver::http::http_utils::INTERNAL_SELECT) + .max_threads(atoi(argv[2]))}; + + auto hwr = std::make_shared(); + ws.register_path(kPath, hwr); + + ws.start(true); + + return 0; +} diff --git a/examples/benchmark_threads.cpp b/examples/benchmark_threads.cpp new file mode 100755 index 000000000..f5e926cd0 --- /dev/null +++ b/examples/benchmark_threads.cpp @@ -0,0 +1,51 @@ +/* + 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 + +static constexpr const char* kPath = "/plaintext"; +static constexpr const char* kBody = "Hello, World!"; + +class hello_world_resource : public httpserver::http_resource { + public: + // TASK-036: http_response is a movable value type; build it + // per-request rather than caching a shared instance. + httpserver::http_response render(const httpserver::http_request&) override { + return httpserver::http_response::string(kBody) + .with_header("Server", "libhttpserver"); + } +}; + +int main(int argc, char** argv) { + std::ignore = argc; + + httpserver::webserver ws{httpserver::create_webserver(atoi(argv[1])) + .start_method(httpserver::http::http_utils::THREAD_PER_CONNECTION)}; + + auto hwr = std::make_shared(); + ws.register_path(kPath, hwr); + + ws.start(true); + + return 0; +} diff --git a/examples/binary_buffer_response.cpp b/examples/binary_buffer_response.cpp new file mode 100644 index 000000000..4bd65f9d5 --- /dev/null +++ b/examples/binary_buffer_response.cpp @@ -0,0 +1,63 @@ +/* + This file is part of libhttpserver + Copyright (C) 2011-2025 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 +*/ + +// binary_buffer_response.cpp - serve binary data (e.g., images) directly +// from an in-memory buffer. std::string holds arbitrary bytes, including +// null characters, making it suitable for any binary content. +// +// To test: +// curl -o output.png http://localhost:8080/image + +#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)); +} + +int main() { + httpserver::webserver ws{httpserver::create_webserver(8080)}; + + ws.on_get("/image", [](const httpserver::http_request&) { + // The response sends the exact bytes in the string — the size is + // tracked internally, so null bytes and non-printable characters + // are transmitted correctly. + return httpserver::http_response::string(generate_png_data(), "image/png"); + }); + + ws.start(true); + return 0; +} diff --git a/examples/centralized_authentication.cpp b/examples/centralized_authentication.cpp new file mode 100644 index 000000000..95a05e9bd --- /dev/null +++ b/examples/centralized_authentication.cpp @@ -0,0 +1,117 @@ +/* + This file is part of libhttpserver + Copyright (C) 2011-2025 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 +*/ + +// centralized_authentication.cpp - install a server-wide auth_handler +// that runs before every request. The handler returns std::nullopt to +// accept, or an http_response to reject. auth_skip_paths lists routes +// that bypass auth entirely. +// +// SECURITY NOTE: Credentials MUST NOT be hardcoded in source code (CWE-798). +// This example loads AUTH_USER and AUTH_PASS from environment variables. +// +// CONSTANT-TIME NOTE: The credential comparison below uses +// `constant_time_equal` (defined further down), which is the +// production-ready form: it runs in time independent of the secret +// bytes, closing the CWE-208 timing side-channel that a naive +// std::string::operator!= (short-circuiting on the first differing +// byte) would open. +// +// Usage: +// # Set credentials and start the server +// export AUTH_USER=myuser AUTH_PASS=mysecretpassword +// ./centralized_authentication +// +// # Without auth - should get 401 Unauthorized +// curl -v http://localhost:8080/api +// +// # With valid auth - should get 200 OK +// curl -u myuser:mysecretpassword http://localhost:8080/api +// +// # Health endpoint (skip path) - works without auth +// curl http://localhost:8080/health + +#include +#include +#include +#include +#include +#include + +#include + +namespace { + +// Constant-time equality: the runtime depends only on a.size(), never on +// where a mismatch occurs, closing the CWE-208 timing side-channel. The +// length is not the secret here, so an early length check is fine (this +// mirrors OpenSSL's CRYPTO_memcmp, which also requires equal lengths). +bool constant_time_equal(std::string_view a, std::string_view b) { + if (a.size() != b.size()) return false; + volatile unsigned char diff = 0; + for (std::size_t i = 0; i < a.size(); ++i) { + diff |= static_cast(a[i] ^ b[i]); + } + return diff == 0; +} + +} // namespace + +// Returns std::nullopt to allow the request, or an engaged optional +// carrying an http_response to reject it (TASK-054). For illustration; +// production must read AUTH_USER and AUTH_PASS once at startup into +// immutable storage -- calling std::getenv on every request is not +// thread-safe with respect to concurrent setenv/putenv from other +// threads in the same process. +std::optional auth_handler( + const httpserver::http_request& req) { + const char* expected_user = std::getenv("AUTH_USER"); + const char* expected_pass = std::getenv("AUTH_PASS"); + if (!expected_user || !expected_pass) { + std::cerr << "centralized_authentication: AUTH_USER and AUTH_PASS " + "environment variables must be set.\n"; + return httpserver::http_response::string("Server configuration error") + .with_status(500); + } + // The || short-circuits at the *field* level (user then pass), which + // does not leak per-byte password timing -- each field compare is + // itself constant-time. + if (!constant_time_equal(req.get_user(), expected_user) || + !constant_time_equal(req.get_pass(), expected_pass)) { + return httpserver::http_response::unauthorized( + "Basic", "MyRealm", "Unauthorized"); + } + return std::nullopt; +} + +int main() { + httpserver::webserver ws{httpserver::create_webserver(8080) + .auth_handler(auth_handler) + .auth_skip_paths({"/health", "/public/*"})}; + + ws.on_get("/api", [](const httpserver::http_request&) { + return httpserver::http_response::string("Hello, authenticated user!"); + }); + ws.on_get("/health", [](const httpserver::http_request&) { + return httpserver::http_response::string("OK"); + }); + + ws.start(true); + return 0; +} diff --git a/examples/clf_access_log.cpp b/examples/clf_access_log.cpp new file mode 100644 index 000000000..1f2a8fd64 --- /dev/null +++ b/examples/clf_access_log.cpp @@ -0,0 +1,135 @@ +/* + This file is part of libhttpserver + Copyright (C) 2011-2026 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 +*/ + +// clf_access_log.cpp -- Common Log Format access logger written as a +// response_sent lifecycle hook, demonstrating the resolution of issues +// #281 and #69. libhttpserver v1's log_access(fn) callback handed the +// user a single string and was invoked at request-arrival time, so the +// status code, byte count, and request duration were not yet known. +// Issues #281 and #69 asked for a logger that could emit a real CLF / +// `time-taken` line. +// +// In v2.0 the response_sent hook (DR-012 §4.10) fires immediately after +// MHD_queue_response and carries the structured context users have been +// asking for: +// - ctx.status -- HTTP status code +// - ctx.bytes_queued -- logical body size (Content-Length, when known) +// - ctx.elapsed -- steady_clock nanoseconds from +// answer_to_connection's first invocation +// +// This example uses those fields to emit a Common Log Format line on +// stdout. The library no longer hard-codes a logging format -- you do. +// +// Run: +// ./clf_access_log # blocks; serves on :8080 +// curl http://localhost:8080/hello +// curl http://localhost:8080/hello?name=world +// +// Expected output (per request): +// - - - [26/May/2026:14:01:23 +0000] "GET /hello HTTP/1.1" 200 13 1 + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace hs = httpserver; + +namespace { + +class hello_resource : public hs::http_resource { + public: + hs::http_response render_get(const hs::http_request&) override { + return hs::http_response::string("Hello, World!"); + } +}; + +// SECURITY (CWE-117): sanitize a string_view for CLF output. +// Replace ASCII control characters (< 0x20 or == 0x7F) with '-' to +// prevent a client from injecting additional log lines by embedding +// newlines or carriage-returns in the request path or method. +std::string sanitize_clf(std::string_view sv) { + std::string out; + out.reserve(sv.size()); + for (unsigned char c : sv) { + // '"' is also replaced: an unescaped double-quote inside a CLF + // quoted field ("METHOD PATH VERSION") would prematurely close + // the quoted-string and corrupt the log line's structure. + out += (c < 0x20 || c == 0x7f || c == '"') ? '-' : static_cast(c); + } + return out; +} + +void emit_clf_line(const hs::response_sent_ctx& ctx) { + // ctx.request is guaranteed non-null at the response_sent fire site + // (documented in response_sent_ctx). Guard here for defence-in-depth. + if (ctx.request == nullptr) return; + + std::time_t now = std::time(nullptr); + char ts[32]; + std::tm tm_buf; +#if defined(_WIN32) && !defined(__CYGWIN__) + localtime_s(&tm_buf, &now); +#else + localtime_r(&now, &tm_buf); +#endif + std::strftime(ts, sizeof(ts), "%d/%b/%Y:%H:%M:%S %z", &tm_buf); + + int64_t ms = std::chrono::duration_cast( + ctx.elapsed).count(); + // Sanitize before embedding in the CLF line to prevent log injection. + // The protocol version is the one the client actually advertised, + // read from ctx.request->get_version() (TASK-018). It is sanitized + // like method and path for defence-in-depth even though it comes from + // MHD's request-line parse. + const std::string method = sanitize_clf(ctx.request->get_method()); + const std::string path = sanitize_clf(ctx.request->get_path()); + const std::string version = sanitize_clf(ctx.request->get_version()); + std::printf("- - - [%s] \"%s %s %s\" %d %zu %lld\n", + ts, + method.c_str(), + path.c_str(), + version.c_str(), + ctx.status, + ctx.bytes_queued, + static_cast(ms)); // NOLINT(runtime/int) + std::fflush(stdout); +} + +} // namespace + +int main() { + hs::webserver ws{hs::create_webserver(8080)}; + + auto h = ws.add_hook(hs::hook_phase::response_sent, + std::function(emit_clf_line)); + + auto resource = std::make_shared(); + ws.register_path("/hello", resource); + + ws.start(true); + return 0; +} diff --git a/examples/client_cert_auth.cpp b/examples/client_cert_auth.cpp new file mode 100644 index 000000000..14759b809 --- /dev/null +++ b/examples/client_cert_auth.cpp @@ -0,0 +1,184 @@ +/* + 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 + +#include + +// Set of allowed certificate fingerprints (SHA-256, hex-encoded). +// For illustration; production must load allowed fingerprints from a +// database or config file, not an in-process set. +std::set allowed_fingerprints; + +// Resource that requires client certificate authentication +class secure_resource : public httpserver::http_resource { + public: + httpserver::http_response render_get(const httpserver::http_request& req) override { + // Check if client provided a certificate + if (!req.has_client_certificate()) { + return httpserver::http_response::string("Client certificate required").with_status(httpserver::http::http_utils::http_unauthorized); + } + + // 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 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 httpserver::http_response::string("Certificate not in allowlist").with_status(httpserver::http::http_utils::http_forbidden); + } + + // Check certificate validity times. TASK-019 narrows the + // accessor return type to std::int64_t. + // + // IMPORTANT: these time checks are safe only because we have already + // confirmed has_client_certificate() == true (line 72) and + // is_client_cert_verified() == true (line 87) above. Without those + // earlier guards, get_client_cert_not_before() / get_client_cert_not_after() + // return the sentinel -1 (a large negative int64_t), and any non-negative + // `now` would satisfy `now > not_after`, producing a misleading + // "Certificate has expired" response instead of denying the request + // due to the absence of a certificate. Never call the time accessors + // without first confirming cert presence and verification. + // (security-reviewer item 21) + std::int64_t now = static_cast(time(nullptr)); + 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 httpserver::http_response::string("Certificate not yet valid").with_status(httpserver::http::http_utils::http_forbidden); + } + + if (now > not_after) { + return httpserver::http_response::string("Certificate has expired").with_status(httpserver::http::http_utils::http_forbidden); + } + + // 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 httpserver::http_response::string(response); + } +}; + +// Public resource that shows certificate info but doesn't require it +class info_resource : public httpserver::http_resource { + public: + httpserver::http_response render_get(const httpserver::http_request& req) override { + std::string response; + + if (req.has_client_certificate()) { + response = "Client certificate detected:\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 httpserver::http_response::string(response); + } +}; + +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 + + auto secure = std::make_shared(); + auto info = std::make_shared(); + + 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"; + 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/examples/comet.cpp b/examples/comet.cpp deleted file mode 100755 index 33eb3d7c0..000000000 --- a/examples/comet.cpp +++ /dev/null @@ -1,66 +0,0 @@ -/* - 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 namespace httpserver; - -std::string topics_array[] = { "topic_1" }; -std::vector topics(topics_array, topics_array + sizeof(topics_array) / sizeof(std::string)); - -class comet_send_resource : public http_resource { - public: - const http_response render(const http_request& req) - { - return http_response_builder("Hi", 200).long_polling_send_response(topics_array[0]); - } -}; - -class comet_listen_resource : public http_resource { - public: - const http_response render(const http_request& req) - { - return http_response_builder("OK", 200).long_polling_receive_response(topics); - } -}; - -int main() -{ - //it is possible to create a webserver passing a great number of parameters. - //In this case we are just passing the port and the number of thread running. - webserver ws = create_webserver(8080).comet(); - - comet_send_resource csr; - comet_listen_resource clr; - //this way we are registering the hello_world_resource to answer for the endpoint - //"/hello". The requested method is called (if the request is a GET we call the render_GET - //method. In case that the specific render method is not implemented, the generic "render" - //method is called. - ws.register_resource("/send", &csr, true); - ws.register_resource("/listen", &clr, true); - - //This way we are putting the created webserver in listen. We pass true in order to have - //a blocking call; if we want the call to be non-blocking we can just pass false to the - //method. - ws.start(true); - return 0; -} diff --git a/examples/custom_access_log.cpp b/examples/custom_access_log.cpp new file mode 100644 index 000000000..f9726e14d --- /dev/null +++ b/examples/custom_access_log.cpp @@ -0,0 +1,47 @@ +/* + This file is part of libhttpserver + Copyright (C) 2011-2025 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 +*/ + +// custom_access_log.cpp - install a per-request access-log callback via +// create_webserver().log_access(). The callback is server-wide; the +// handler itself is a plain lambda. + +#include +#include + +#include + +void custom_access_log(const std::string& url) { + // NOTE: In production, sanitize the URL before writing to a terminal or + // log file — raw URLs may contain ANSI escape sequences or control + // characters that can corrupt terminal output (CWE-117). + std::cout << "ACCESSING: " << url << std::endl; +} + +int main() { + httpserver::webserver ws{httpserver::create_webserver(8080) + .log_access(custom_access_log)}; + + ws.on_get("/hello", [](const httpserver::http_request&) { + return httpserver::http_response::string("Hello, World!"); + }); + + ws.start(true); + return 0; +} diff --git a/examples/custom_error.cpp b/examples/custom_error.cpp new file mode 100644 index 000000000..14f5b691d --- /dev/null +++ b/examples/custom_error.cpp @@ -0,0 +1,52 @@ +/* + 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 + +httpserver::http_response not_found_custom(const httpserver::http_request&) { + return httpserver::http_response::string("Not found custom").with_status(404); +} + +httpserver::http_response not_allowed_custom(const httpserver::http_request&) { + return httpserver::http_response::string("Not allowed custom").with_status(405); +} + +class hello_world_resource : public httpserver::http_resource { + public: + httpserver::http_response render(const httpserver::http_request&) override { + return httpserver::http_response::string("Hello, World!"); + } +}; + +int main() { + httpserver::webserver ws{httpserver::create_webserver(8080) + .not_found_handler(not_found_custom) + .method_not_allowed_handler(not_allowed_custom)}; + + 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 new file mode 100644 index 000000000..07f4d0477 --- /dev/null +++ b/examples/daemon_info.cpp @@ -0,0 +1,53 @@ +/* + This file is part of libhttpserver + Copyright (C) 2011-2025 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 +*/ + +// daemon_info.cpp - introspect the running daemon (bound port, listen FD, +// active connections, etc.) after start(false). Port 0 lets the OS pick +// an ephemeral port so multiple instances can coexist. + +#include + +#include + +int main() { + httpserver::webserver ws{httpserver::create_webserver(0)}; + + ws.on_get("/hello", [](const httpserver::http_request&) { + return httpserver::http_response::string("Hello, World!"); + }); + + ws.start(false); + + std::cout << "libmicrohttpd version: " + << httpserver::http::http_utils::get_mhd_version() << std::endl; + std::cout << "Bound port: " << ws.get_bound_port() << std::endl; + std::cout << "Listen FD: " << ws.get_listen_fd() << std::endl; + std::cout << "Active connections: " << ws.get_active_connections() << std::endl; + std::cout << "HTTP 200 reason: " + << httpserver::http::http_utils::reason_phrase(200) << std::endl; + std::cout << "HTTP 404 reason: " + << httpserver::http::http_utils::reason_phrase(404) << std::endl; + + std::cout << "\nServer running on port " << ws.get_bound_port() + << ". Press Ctrl+C to stop." << std::endl; + + ws.stop_and_wait(); + return 0; +} diff --git a/examples/deferred_with_accumulator.cpp b/examples/deferred_with_accumulator.cpp new file mode 100644 index 000000000..8800a38ea --- /dev/null +++ b/examples/deferred_with_accumulator.cpp @@ -0,0 +1,78 @@ +/* + 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 +// 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 + +// Global counter: tracks how many connections have been established. +// Each render_get() call captures a snapshot (reqid) by value. +std::atomic counter; + +class deferred_resource : public httpserver::http_resource { + public: + httpserver::http_response render_get(const httpserver::http_request&) override { + int reqid = counter++; + std::string preamble = "cycle callback response"; + return + httpserver::http_response::deferred( + [reqid, preamble, + served = false](std::uint64_t, char* buf, + std::size_t max) mutable -> ssize_t { + if (!served) { + served = true; + std::size_t n = std::min(preamble.size(), max); + memcpy(buf, preamble.data(), n); + return n; + } + // only first 5 connections can be established + if (reqid >= 5) { + return -1; + } + // respond corresponding request IDs to the clients + std::string str = std::to_string(reqid) + " "; + memset(buf, 0, max); + std::copy(str.begin(), str.end(), buf); + // keep sending reqid + std::this_thread::sleep_for(std::chrono::seconds(1)); + return static_cast(max); + }); + } +}; + +int main() { + httpserver::webserver ws{httpserver::create_webserver(8080)}; + + auto hwr = std::make_shared(); + ws.register_path("/hello", hwr); + ws.start(true); + + return 0; +} + diff --git a/examples/denied_ip_log.cpp b/examples/denied_ip_log.cpp new file mode 100644 index 000000000..689eb40c9 --- /dev/null +++ b/examples/denied_ip_log.cpp @@ -0,0 +1,76 @@ +/* + This file is part of libhttpserver + Copyright (C) 2011-2026 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 +*/ + +// Demonstrates the solution to issue #332: log every denied-IP rejection. +// +// Configure the daemon with an ACCEPT default policy, deny the IPs you +// want refused via ws.deny_ip(), and register a connection-level +// `accept_decision` hook that logs every rejection to stderr. +// +// The hook is observation-only: throwing from it does not flip the +// decision (the daemon still rejects the connection per the policy +// callback's return), so it is safe to do I/O here. +// +// Run, then attempt to connect from one of the denied IPs to see +// stderr show a "[DENIED] ..." line per attempt. + +#include +#include +#include +#include + +#include + +namespace hs = httpserver; + +class hello_resource : public hs::http_resource { + public: + hs::http_response render_get(const hs::http_request&) override { + return hs::http_response::string("Hello, World!"); + } +}; + +int main() { + hs::webserver ws{hs::create_webserver(8080) + .default_policy(hs::http::http_utils::ACCEPT)}; + + // Deny whatever client IP you want refused. The shipping example + // uses an RFC-1918 placeholder; swap with your client's address. + ws.deny_ip("10.0.0.1"); + + auto h = ws.add_hook(hs::hook_phase::accept_decision, + std::function( + [](const hs::accept_ctx& ctx) { + if (!ctx.accepted) { + std::cerr << "[DENIED] peer=" << ctx.peer.to_string() + << " reason=" + << (ctx.reason.has_value() + ? std::string(*ctx.reason) + : std::string("unspecified")) + << std::endl; + } + })); + + auto resource = std::make_shared(); + ws.register_path("/hello", resource); + + ws.start(true); // blocking + return 0; +} diff --git a/examples/digest_authentication.cpp b/examples/digest_authentication.cpp new file mode 100644 index 000000000..8fd29a4a8 --- /dev/null +++ b/examples/digest_authentication.cpp @@ -0,0 +1,74 @@ +/* + This file is part of libhttpserver + Copyright (C) 2011-2025 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 +*/ + +// digest_authentication.cpp - per-request HTTP Digest auth check inside +// a lambda handler. The two `unauthorized` returns are intentionally +// indistinguishable to the client to avoid leaking nonce-state info. +// +// NOTE: SHA-256 is used here (RFC 7616). MD5-based Digest auth is broken +// (trivially cracked by offline dictionary attacks against the HA1 hash) and +// must not be used in new deployments. Use SHA-256 or stronger. +// +// The 300-second nonce lifetime balances usability (clients can retry without +// re-authenticating) against replay-attack window. Shorten for higher security. +// +// SECURITY NOTE: Credentials MUST NOT be hardcoded in source code (CWE-798). +// This example loads DIGEST_PASS from the environment. +// +// Usage: +// export DIGEST_PASS=mysecretpassword +// ./digest_authentication + +#include +#include +#include + +#include + +int main() { + const char* expected_pass = std::getenv("DIGEST_PASS"); + if (!expected_pass) { + std::cerr << "digest_authentication: DIGEST_PASS environment " + "variable must be set.\n"; + return 1; + } + const std::string pass = expected_pass; + + httpserver::webserver ws{httpserver::create_webserver(8080)}; + + ws.on_get("/hello", [pass](const httpserver::http_request& req) { + using httpserver::http::http_utils; + if (req.get_digested_user() == "") { + return httpserver::http_response::unauthorized( + "Digest", "test@example.com", "FAIL"); + } + auto result = req.check_digest_auth( + "test@example.com", pass, 300, 0, + http_utils::digest_algorithm::SHA256); + if (result != http_utils::digest_auth_result::OK) { + return httpserver::http_response::unauthorized( + "Digest", "test@example.com", "FAIL"); + } + return httpserver::http_response::string("SUCCESS"); + }); + + ws.start(true); + return 0; +} diff --git a/examples/early_413.cpp b/examples/early_413.cpp new file mode 100644 index 000000000..2f63c7d84 --- /dev/null +++ b/examples/early_413.cpp @@ -0,0 +1,74 @@ +/* + This file is part of libhttpserver + Copyright (C) 2011-2026 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 +*/ + +// Demonstrates the solution to issue #273: short-circuit large uploads +// with a 413 BEFORE any body bytes are consumed. +// +// Register a `request_received` hook that inspects Content-Length; if +// it exceeds the configured cap, return a respond_with(413) action. +// libhttpserver aborts the upload -- the resource handler is not +// invoked and the body bytes never cross the daemon's I/O boundary. + +#include +#include +#include +#include + +#include + +namespace hs = httpserver; + +namespace { +constexpr std::size_t kMaxUploadBytes = 1 * 1024 * 1024; // 1 MB +} // namespace + +class upload_resource : public hs::http_resource { + public: + hs::http_response render_post(const hs::http_request&) override { + return hs::http_response::string("UPLOAD OK"); + } +}; + +int main() { + hs::webserver ws{hs::create_webserver(8080)}; + + auto h = ws.add_hook(hs::hook_phase::request_received, + std::function( + [](hs::request_received_ctx& ctx) { + std::string cl{ctx.request->get_header("Content-Length")}; + if (cl.empty()) return hs::hook_action::pass(); + try { + if (std::stoull(cl) > kMaxUploadBytes) { + return hs::hook_action::respond_with( + hs::http_response::empty().with_status(413)); + } + } catch (...) { + // Malformed Content-Length: let the normal pipeline + // produce a 400 elsewhere. + } + return hs::hook_action::pass(); + })); + + auto resource = std::make_shared(); + ws.register_path("/upload", resource); + + ws.start(true); // blocking + return 0; +} diff --git a/examples/empty_response_example.cpp b/examples/empty_response_example.cpp new file mode 100644 index 000000000..22e4a1879 --- /dev/null +++ b/examples/empty_response_example.cpp @@ -0,0 +1,47 @@ +/* + 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 + +class no_content_resource : public httpserver::http_resource { + public: + httpserver::http_response render_delete(const httpserver::http_request&) override { + return httpserver::http_response::empty(); + } + + httpserver::http_response render_head(const httpserver::http_request&) override { + // libhttpserver strips the body automatically for HEAD requests. + return httpserver::http_response::empty() + .with_status(httpserver::http::http_utils::http_ok) + .with_header("X-Total-Count", "42"); + } +}; + +int main() { + httpserver::webserver ws{httpserver::create_webserver(8080)}; + + 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 new file mode 100644 index 000000000..828cb24cc --- /dev/null +++ b/examples/external_event_loop.cpp @@ -0,0 +1,62 @@ +/* + This file is part of libhttpserver + Copyright (C) 2011-2025 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 +*/ + +// external_event_loop.cpp - run the daemon under EXTERNAL_SELECT and +// drive it via run_wait() from the application's own loop. The +// thread_safety(false) optimization is intentionally omitted for +// portability (some MHD builds reject the combination at start). + +#include +#include + +#include + +static volatile bool running = true; + +void signal_handler(int) { + running = false; +} + +int main() { + signal(SIGINT, signal_handler); + + httpserver::webserver ws{httpserver::create_webserver(8080) + .start_method(httpserver::http::http_utils::EXTERNAL_SELECT)}; + + ws.on_get("/hello", [](const httpserver::http_request&) { + return httpserver::http_response::string("Hello from external event loop!"); + }); + + ws.start(false); + + std::cout << "Server running on port " << ws.get_bound_port() << std::endl; + + while (running) { + ws.run_wait(1000); + // Do other work between iterations as needed. + } + + std::cout << "\nShutting down..." << std::endl; + + // Graceful shutdown: stop accepting new connections first. + ws.quiesce(); + ws.stop(); + return 0; +} diff --git a/examples/file_upload.cpp b/examples/file_upload.cpp new file mode 100644 index 000000000..dac870ac8 --- /dev/null +++ b/examples/file_upload.cpp @@ -0,0 +1,141 @@ +/* + 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 + +// HTML-escape user-controlled text before placing it in a response body +// to prevent reflected XSS (CWE-79). Never echo unfiltered client input. +static std::string html_escape(std::string_view in) { + std::string out; + out.reserve(in.size()); + for (char c : in) { + switch (c) { + case '&': out += "&"; break; + case '<': out += "<"; break; + case '>': out += ">"; break; + case '"': out += """; break; + case '\'': out += "'"; break; + default: out += c; break; + } + } + return out; +} + +class file_upload_resource : public httpserver::http_resource { + public: + httpserver::http_response render_get(const httpserver::http_request&) override { + return httpserver::http_response::string(R"html( + +
+

Upload 1 (key is 'files', multiple files can be selected)


+ +

+

Upload 2 (key is 'files2', multiple files can be selected)


+

+ +
+ + +)html", "text/html"); + } + + httpserver::http_response render_post(const httpserver::http_request& req) override { + // Static header built from a raw string literal to avoid per-append + // reallocations. The dynamic rows are appended after an upfront reserve. + std::string post_response = R"html( + + + + + Uploaded files: +

+ + + + + + + + + +)html"; + post_response.reserve(post_response.size() + 512); + + for (auto &file_key : req.get_files()) { + for (auto &files : file_key.second) { + post_response += " \n"; + } + } + + post_response += "
KeyUploaded filenameFile system pathFile sizeContent typeTransfer encoding
"; + post_response += html_escape(file_key.first); + post_response += ""; + post_response += html_escape(files.first); + post_response += ""; + post_response += html_escape(files.second.get_file_system_file_name()); + post_response += ""; + post_response += std::to_string(files.second.get_file_size()); + post_response += ""; + post_response += html_escape(files.second.get_content_type()); + post_response += ""; + post_response += html_escape(files.second.get_transfer_encoding()); + post_response += "


\n"; + post_response += " back\n"; + post_response += "\n"; + return httpserver::http_response::string(post_response, "text/html").with_status(201); + } +}; + +int main(int argc, char** argv) { + // this example needs a directory as parameter + if (2 != argc) { + std::cout << "Usage: file_upload " << std::endl; + std::cout << std::endl; + std::cout << " file_upload: writeable directory where uploaded files will be stored" << std::endl; + return -1; + } + + std::cout << "CAUTION: this example will create files in the directory " << std::string(argv[1]) << std::endl; + std::cout << "These files won't be deleted at termination" << std::endl; + std::cout << "Please make sure, that the given directory exists and is writeable" << std::endl; + + httpserver::webserver ws{httpserver::create_webserver(8080) + .put_processed_data_to_content(false) + .file_upload_dir(std::string(argv[1])) + .generate_random_filename_on_upload() + .file_upload_target(httpserver::FILE_UPLOAD_DISK_ONLY)}; + + auto fur = std::make_shared(); + ws.register_path("/", fur); + ws.start(true); + + return 0; +} + diff --git a/examples/file_upload_with_callback.cpp b/examples/file_upload_with_callback.cpp new file mode 100644 index 000000000..9d83f70e8 --- /dev/null +++ b/examples/file_upload_with_callback.cpp @@ -0,0 +1,155 @@ +/* + 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 + +#include + +// HTML-escape user-controlled text before placing it in a response body +// to prevent reflected XSS (CWE-79). Never echo unfiltered client input. +static std::string html_escape(std::string_view in) { + std::string out; + out.reserve(in.size()); + for (char c : in) { + switch (c) { + case '&': out += "&"; break; + case '<': out += "<"; break; + case '>': out += ">"; break; + case '"': out += """; break; + case '\'': out += "'"; break; + default: out += c; break; + } + } + return out; +} + +// Reject filenames containing path separators, '..' segments, or NUL bytes +// to prevent path traversal (CWE-22) when joining with permanent_dir. +static bool is_safe_filename(std::string_view name) { + if (name.empty()) return false; + if (name == "." || name == "..") return false; + if (name.find('/') != std::string_view::npos) return false; + if (name.find('\\') != std::string_view::npos) return false; + if (name.find('\0') != std::string_view::npos) return false; + return true; +} + +class file_upload_resource : public httpserver::http_resource { + public: + httpserver::http_response render_get(const httpserver::http_request&) override { + return httpserver::http_response::string(R"html( + +

File Upload with Cleanup Callback Demo

+

Uploaded files will be moved to the permanent directory.

+
+ +

+ +
+ + +)html", "text/html"); + } + + httpserver::http_response render_post(const httpserver::http_request& req) override { + std::string post_response = R"html( + +

Upload Complete

+

Files have been moved to permanent storage:

+
    +)html"; + post_response.reserve(post_response.size() + 256); + + for (auto &file_key : req.get_files()) { + for (auto &files : file_key.second) { + post_response += "
  • "; + post_response += html_escape(files.first); + post_response += " ("; + post_response += std::to_string(files.second.get_file_size()); + post_response += " bytes)
  • \n"; + } + } + + post_response += "
\n"; + post_response += " Upload more\n"; + post_response += "\n"; + return httpserver::http_response::string(post_response, "text/html").with_status(201); + } +}; + +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 + // Validate the filename before joining with permanent_dir. + // generate_random_filename_on_upload() already gives us a + // server-generated name, but a defensive check here protects + // against future code changes that may pass a client-supplied + // name through. + if (!is_safe_filename(filename)) { + std::cerr << "Rejected unsafe filename, will be deleted" + << std::endl; + return true; + } + // 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 + } + })}; + + auto fur = std::make_shared(); + ws.register_path("/", fur); + ws.start(true); + + return 0; +} diff --git a/examples/handlers.cpp b/examples/handlers.cpp new file mode 100644 index 000000000..427fd4df4 --- /dev/null +++ b/examples/handlers.cpp @@ -0,0 +1,40 @@ +/* + This file is part of libhttpserver + Copyright (C) 2011-2025 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 +*/ + +// handlers.cpp - register distinct lambda handlers for two HTTP methods +// on the same path. The webserver composes them: a GET to /hello dispatches +// to the first lambda, a POST to the second, and any other method gets the +// default 405 Method Not Allowed response. + +#include + +int main() { + httpserver::webserver ws{httpserver::create_webserver(8080)}; + + ws.on_get("/hello", [](const httpserver::http_request&) { + return httpserver::http_response::string("GET: Hello, World!"); + }); + ws.on_post("/hello", [](const httpserver::http_request&) { + return httpserver::http_response::string("POST: Hello, World!"); + }); + + ws.start(true); + return 0; +} diff --git a/examples/hello_with_get_arg.cpp b/examples/hello_with_get_arg.cpp new file mode 100644 index 000000000..c8665901e --- /dev/null +++ b/examples/hello_with_get_arg.cpp @@ -0,0 +1,49 @@ +/* + This file is part of libhttpserver + Copyright (C) 2011-2025 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 +*/ + +// hello_with_get_arg.cpp - read a query-string argument inside a lambda +// handler. Try: curl 'http://localhost:8080/hello?name=world' +// +// NOTE: The response is plain text. If you adapt this for an HTML response, +// you must HTML-entity-encode user-supplied input before inserting it into +// the document body — raw reflection into HTML introduces reflected XSS +// (CWE-79). The explicit "text/plain" content type below signals that +// context and prevents browsers from sniffing the body as HTML. + +#include + +#include + +int main() { + httpserver::webserver ws{httpserver::create_webserver(8080)}; + + ws.on_get("/hello", [](const httpserver::http_request& req) { + // Explicit "text/plain" avoids browser content-type sniffing. + // Never reflect user input into an HTML response without encoding. + std::string body; + body.reserve(7 + req.get_arg("name").get_flat_value().size()); + body = "Hello: "; + body += req.get_arg("name"); + return httpserver::http_response::string(body, "text/plain"); + }); + + ws.start(true); + return 0; +} diff --git a/examples/hello_world.cpp b/examples/hello_world.cpp index 9a0a4e4c7..71b83b079 100755 --- a/examples/hello_world.cpp +++ b/examples/hello_world.cpp @@ -1,66 +1,17 @@ -/* - 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 -*/ - +// Copyright 2026 Sebastiano Merlino +// libhttpserver hello-world example — the lambda form. +// Compiles in ten lines including main(), with no http_resource subclass +// and no raw-pointer ownership. Production code typically qualifies names +// explicitly; the `using namespace` here is a one-off so this file can +// document the shortest possible end-to-end demo. See shared_state.cpp +// for the class-based pattern that is appropriate when handlers must +// share mutable state. #include -#include - -using namespace httpserver; - -class hello_world_resource : public http_resource { - public: - const http_response render(const http_request&); - void set_some_data(const std::string &s) {data = s;} - std::string data; -}; - -//using the render method you are able to catch each type of request you receive -const http_response hello_world_resource::render(const http_request& req) -{ - //it is possible to store data inside the resource object that can be altered - //through the requests - std::cout << "Data was: " << data << std::endl; - std::string datapar = req.get_arg("data"); - set_some_data(datapar == "" ? "no data passed!!!" : datapar); - std::cout << "Now data is:" << data << std::endl; - - //it is possible to send a response initializing an http_string_response - //that reads the content to send in response from a string. - return http_response_builder("Hello World!!!", 200).string_response(); -} - -int main() -{ - //it is possible to create a webserver passing a great number of parameters. - //In this case we are just passing the port and the number of thread running. - webserver ws = create_webserver(8080).start_method(http::http_utils::INTERNAL_SELECT).max_threads(5); - - hello_world_resource hwr; - //this way we are registering the hello_world_resource to answer for the endpoint - //"/hello". The requested method is called (if the request is a GET we call the render_GET - //method. In case that the specific render method is not implemented, the generic "render" - //method is called. - ws.register_resource("/hello", &hwr, true); - - //This way we are putting the created webserver in listen. We pass true in order to have - //a blocking call; if we want the call to be non-blocking we can just pass false to the - //method. +using namespace httpserver; // NOLINT(build/namespaces) - keep the demo at <=10 LOC +int main() { + webserver ws{create_webserver(8080)}; + ws.on_get("/hello", [](const http_request&) { + return http_response::string("Hello, World!"); + }); ws.start(true); - return 0; } diff --git a/examples/iovec_response_example.cpp b/examples/iovec_response_example.cpp new file mode 100644 index 000000000..97d695997 --- /dev/null +++ b/examples/iovec_response_example.cpp @@ -0,0 +1,50 @@ +/* + This file is part of libhttpserver + Copyright (C) 2011-2025 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 +*/ + +// iovec_response_example.cpp - build a response body from multiple +// borrowed buffers without copying. The buffers must outlive the +// response; static-lifetime literals satisfy that contract trivially. + +#include + +#include + +static const char kPart1[] = "{\"header\": \"value\", "; +static const char kPart2[] = "\"items\": [1, 2, 3], "; +static const char kPart3[] = "\"footer\": \"end\"}"; + +int main() { + httpserver::webserver ws{httpserver::create_webserver(8080)}; + + ws.on_get("/data", [](const httpserver::http_request&) { + std::vector parts = { + { kPart1, sizeof(kPart1) - 1 }, + { kPart2, sizeof(kPart2) - 1 }, + { kPart3, sizeof(kPart3) - 1 }, + }; + return httpserver::http_response::iovec(parts) + .with_header("Content-Type", "application/json") + // defense-in-depth: prevent browsers from sniffing content type + .with_header("X-Content-Type-Options", "nosniff"); + }); + + ws.start(true); + return 0; +} diff --git a/examples/minimal_deferred.cpp b/examples/minimal_deferred.cpp new file mode 100644 index 000000000..f651abb7d --- /dev/null +++ b/examples/minimal_deferred.cpp @@ -0,0 +1,51 @@ +/* + This file is part of libhttpserver + Copyright (C) 2011-2025 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 +*/ + +// minimal_deferred.cpp - serve a response body in chunks using a deferred +// callback lambda. The lambda is called repeatedly by libmicrohttpd until it +// returns -1 (signalling end-of-body). The `done` flag ensures the body is +// sent exactly once; the second call returns -1 to close the stream. + +#include +#include +#include +#include + +#include + +int main() { + httpserver::webserver ws{httpserver::create_webserver(8080)}; + + ws.on_get("/hello", [](const httpserver::http_request&) { + return httpserver::http_response::deferred( + [body = std::string("hello from deferred"), + done = false](std::uint64_t, char* buf, + std::size_t max) mutable -> ssize_t { + if (done) return -1; + done = true; + std::size_t n = std::min(body.size(), max); + std::memcpy(buf, body.data(), n); + return static_cast(n); + }); + }); + + ws.start(true); + return 0; +} diff --git a/examples/minimal_file_response.cpp b/examples/minimal_file_response.cpp new file mode 100644 index 000000000..5a67bc2fc --- /dev/null +++ b/examples/minimal_file_response.cpp @@ -0,0 +1,36 @@ +/* + This file is part of libhttpserver + Copyright (C) 2011-2025 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 +*/ + +// minimal_file_response.cpp - stream a file from disk as the response +// body using the http_response::file() factory. + +#include + +int main() { + httpserver::webserver ws{httpserver::create_webserver(8080)}; + + ws.on_get("/hello", [](const httpserver::http_request&) { + return httpserver::http_response::file("test_content") + .with_header("Content-Type", "text/plain"); + }); + + ws.start(true); + return 0; +} diff --git a/examples/minimal_https.cpp b/examples/minimal_https.cpp new file mode 100644 index 000000000..271580218 --- /dev/null +++ b/examples/minimal_https.cpp @@ -0,0 +1,47 @@ +/* + This file is part of libhttpserver + Copyright (C) 2011-2025 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 +*/ + +// minimal_https.cpp - enable HTTPS via the use_ssl / https_mem_key / +// https_mem_cert chain on create_webserver. +// +// The https_priorities call sets an explicit GnuTLS cipher-priority string +// that allows TLS 1.2 and TLS 1.3 with safe renegotiation. Without it, +// GnuTLS may fall back to TLS 1.0/1.1 depending on the system configuration. +// Port 8443 is used (convention for HTTPS on non-privileged ports). + +#include + +int main() { + httpserver::webserver ws{httpserver::create_webserver(8443) + .use_ssl() + .https_mem_key("key.pem") + .https_mem_cert("cert.pem") + .https_priorities( + "NORMAL:-VERS-TLS-ALL" + ":+VERS-TLS1.2:+VERS-TLS1.3" + ":%SAFE_RENEGOTIATION")}; + + ws.on_get("/hello", [](const httpserver::http_request&) { + return httpserver::http_response::string("Hello, World!"); + }); + + ws.start(true); + return 0; +} diff --git a/examples/minimal_https_psk.cpp b/examples/minimal_https_psk.cpp new file mode 100644 index 000000000..ef70d38c2 --- /dev/null +++ b/examples/minimal_https_psk.cpp @@ -0,0 +1,87 @@ +/* + This file is part of libhttpserver + Copyright (C) 2011-2025 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 +*/ + +// minimal_https_psk.cpp - TLS with pre-shared keys instead of certificates. +// The PSK lookup callback is invoked once per handshake to resolve a +// client-supplied identity into the shared secret. + +#include +#include +#include +#include + +#include + +// PSK database loaded from environment variables at startup. +// +// SECURITY NOTE: Never hardcode PSK keys in source code (CWE-321 / CWE-798). +// The values below are placeholders. For illustration; production must: +// - Load keys from environment variables, a key file, or a secrets manager. +// - Use at least 128 bits of cryptographically-random key material. +// - Rotate keys regularly. +// +// Example environment setup before running: +// export PSK_CLIENT1=0123456789abcdef0123456789abcdef +// export PSK_CLIENT2=fedcba9876543210fedcba9876543210 +// ./minimal_https_psk +static std::map build_psk_database() { + std::map db; + const char* k1 = std::getenv("PSK_CLIENT1"); + const char* k2 = std::getenv("PSK_CLIENT2"); + if (!k1 || !k2) { + std::cerr << "minimal_https_psk: PSK_CLIENT1 and PSK_CLIENT2 " + "environment variables must be set.\n"; + std::exit(1); + } + db["client1"] = k1; + db["client2"] = k2; + return db; +} + +static std::map psk_database = build_psk_database(); + +// PSK credential handler callback. Returns the hex-encoded PSK for the +// given username, or an empty string for unknown identities. +std::string psk_handler(const std::string& username) { + auto it = psk_database.find(username); + if (it != psk_database.end()) { + return it->second; + } + return ""; +} + +int main() { + // The priority string locks to TLS 1.2 because GnuTLS external PSK in + // TLS 1.3 requires a different mechanism (session-ticket-based resumption). + // Port 8443 signals HTTPS semantics by convention. + httpserver::webserver ws{ + httpserver::create_webserver(8443) + .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")}; + + ws.on_get("/hello", [](const httpserver::http_request&) { + return httpserver::http_response::string("Hello, World (via TLS-PSK)!"); + }); + + ws.start(true); + return 0; +} diff --git a/examples/minimal_ip_access_control.cpp b/examples/minimal_ip_access_control.cpp new file mode 100644 index 000000000..456f71ccb --- /dev/null +++ b/examples/minimal_ip_access_control.cpp @@ -0,0 +1,53 @@ +/* + This file is part of libhttpserver + Copyright (C) 2011-2025 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 +*/ + +// minimal_ip_access_control.cpp - demonstrate the IP access-control API. +// +// Two lists, selected by create_webserver::default_policy(): +// - deny_ip / remove_denied_ip : the exception list under the default +// ACCEPT policy (permit everyone except these). +// - allow_ip / remove_allowed_ip : the exception list under the REJECT +// policy (permit only these). Under ACCEPT, an allow entry also +// overrides a matching deny entry (allow wins). + +#include + +int main() { + // Default ACCEPT policy: block-list mode. Everyone is admitted except + // addresses on the deny list. + httpserver::webserver ws{httpserver::create_webserver(8080)}; + ws.deny_ip("10.0.0.1"); + + // Allow-list mode: flip the default to REJECT, then permit only the + // addresses you allow_ip. Everyone else is refused at the policy + // callback. + // + // httpserver::webserver ws{ + // httpserver::create_webserver(8080) + // .default_policy(httpserver::http::http_utils::REJECT)}; + // ws.allow_ip("127.0.0.1"); // only localhost may connect + + ws.on_get("/hello", [](const httpserver::http_request&) { + return httpserver::http_response::string("Hello, World!"); + }); + + ws.start(true); + return 0; +} diff --git a/examples/per_route_auth.cpp b/examples/per_route_auth.cpp new file mode 100644 index 000000000..815db6093 --- /dev/null +++ b/examples/per_route_auth.cpp @@ -0,0 +1,161 @@ +/* + This file is part of libhttpserver + Copyright (C) 2011-2026 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 +*/ + +// Demonstrates DR-012 / PRD-HOOK-REQ-006: a `before_handler` hook +// registered via `http_resource::add_hook(...)` is per-route. The hook +// fires only when *this* resource is dispatched -- a sibling route +// registered on the same webserver is not touched. +// +// The example wires two resources: +// +// /public -- always 200 OK, no auth. +// /private -- a per-route before_handler hook checks the Authorization +// header (HTTP Basic). Missing or invalid credentials +// short-circuit with 401; valid credentials let the +// handler run and return 200 OK. +// +// SECURITY NOTE (CWE-798): the expected credentials are loaded from the +// environment (AUTH_USER / AUTH_PASS); do not hardcode them. Set both +// before running: +// +// export AUTH_USER=alice AUTH_PASS=hunter2 +// ./per_route_auth +// +// TRANSPORT NOTE: HTTP Basic auth sends credentials base64-encoded but +// NOT encrypted. In production, serve only over TLS (use +// create_webserver().use_ssl(...)). +// +// CONSTANT-TIME NOTE: The credential comparison below uses +// `constant_time_equal` (defined further down), which is the +// production-ready form: it runs in time independent of the secret +// bytes, closing the CWE-208 timing side-channel that a naive +// std::string_view::operator== (short-circuiting on the first differing +// byte) would open. +// +// Then: +// +// curl -v http://localhost:8080/public # 200 OK +// curl -v http://localhost:8080/private # 401 +// curl -v -u alice:hunter2 http://localhost:8080/private # 200 OK +// +// Contrast with `centralized_authentication.cpp`, which installs the +// server-wide `auth_handler` setter (the v1 alias for a webserver-wide +// before_handler hook). Per-route `add_hook` is the right shape when +// only a subset of routes need protection without dragging the rest of +// the surface through an auth_skip_paths list. + +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace hs = httpserver; + +namespace { + +// Constant-time equality: the runtime depends only on a.size(), never on +// where a mismatch occurs, closing the CWE-208 timing side-channel. The +// length is not the secret here, so an early length check is fine (this +// mirrors OpenSSL's CRYPTO_memcmp, which also requires equal lengths). +bool constant_time_equal(std::string_view a, std::string_view b) { + if (a.size() != b.size()) return false; + volatile unsigned char diff = 0; + for (std::size_t i = 0; i < a.size(); ++i) { + diff |= static_cast(a[i] ^ b[i]); + } + return diff == 0; +} + +class hello_resource : public hs::http_resource { + public: + hs::http_response render_get(const hs::http_request&) override { + return hs::http_response::string("Hello, World!"); + } +}; + +class private_resource : public hs::http_resource { + public: + hs::http_response render_get(const hs::http_request&) override { + return hs::http_response::string("Welcome to the private area."); + } +}; + +} // namespace + +int main() { + const char* expected_user = std::getenv("AUTH_USER"); + const char* expected_pass = std::getenv("AUTH_PASS"); + if (expected_user == nullptr || expected_pass == nullptr) { + std::cerr << "per_route_auth: set AUTH_USER and AUTH_PASS before " + "running.\n"; + return 1; + } + + hs::webserver ws{hs::create_webserver(8080)}; + + auto pub = std::make_shared(); + auto priv = std::make_shared(); + + // The hook is registered on `priv` ONLY. Dispatches against `pub` + // never enter this callback -- a sibling-route observation that is + // the load-bearing property of per-route hooks (DR-012). + std::string user{expected_user}; + std::string pass{expected_pass}; + auto h = priv->add_hook(hs::hook_phase::before_handler, + std::function( + [user, pass](hs::before_handler_ctx& ctx) { + // Null should never occur at before_handler on a matched + // route, but fail closed (deny) rather than pass on any + // unexpected null to avoid a fail-open security hole + // (CWE-636). + if (ctx.request == nullptr) { + return hs::hook_action::respond_with( + hs::http_response::unauthorized( + "Basic", "private-realm", "Unauthorized")); + } + std::string_view u = ctx.request->get_user(); + std::string_view p = ctx.request->get_pass(); + // The && short-circuits at the *field* level (user then + // pass), which does not leak per-byte password timing -- + // each field compare is itself constant-time. + if (constant_time_equal(u, user) && + constant_time_equal(p, pass)) { + return hs::hook_action::pass(); + } + return hs::hook_action::respond_with( + hs::http_response::unauthorized( + "Basic", "private-realm", "Unauthorized")); + })); + + // Keep the handle in scope for the full server lifetime; its + // destructor at scope exit removes the registration. Since main() + // never returns (start(true) blocks), this is the simple shape. + + ws.register_path("/public", pub); + ws.register_path("/private", priv); + + ws.start(true); // blocking + return 0; +} diff --git a/examples/pipe_response_example.cpp b/examples/pipe_response_example.cpp new file mode 100644 index 000000000..5a475d038 --- /dev/null +++ b/examples/pipe_response_example.cpp @@ -0,0 +1,93 @@ +/* + This file is part of libhttpserver + Copyright (C) 2011-2025 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 +*/ + +// pipe_response_example.cpp - stream a response from the read end of a +// pipe that a background thread writes into. The lambda owns the +// short-lived pipe + writer; ownership of the read-end fd transfers +// into the response. + +#if defined(_WIN32) && !defined(__CYGWIN__) +#include +#include +#else +#include +#endif + +#include +#include +#include + +#include + +namespace { +// Production-ready: writes the whole buffer, retrying short writes and +// resuming after an EINTR-interrupted call. Returns false on a hard +// error. On Windows `write`/`errno` are the CRT `_write`/`errno`; EINTR +// never fires there, so the loop is a zero-cost wrapper and stays +// portable. +bool write_all(int fd, const char* buf, size_t len) { + size_t off = 0; + while (off < len) { + ssize_t n = write(fd, buf + off, len - off); + if (n < 0) { + if (errno == EINTR) continue; + return false; + } + if (n == 0) return false; + off += static_cast(n); + } + return true; +} +} // namespace + +int main() { + httpserver::webserver ws{httpserver::create_webserver(8080)}; + + ws.on_get("/stream", [](const httpserver::http_request&) { + int pipefd[2]; +#if defined(_WIN32) && !defined(__CYGWIN__) + if (_pipe(pipefd, 4096, _O_BINARY) == -1) { +#else + if (pipe(pipefd) == -1) { +#endif + return httpserver::http_response::string("pipe failed").with_status(500); + } + + std::thread writer([fd = pipefd[1]]() { + const char* messages[] = {"Hello ", "from ", "a pipe!\n"}; + for (const char* msg : messages) { + // Production-ready: write_all loops to handle partial + // writes and EINTR. Stop streaming on a hard error -- + // closing the write end signals EOF to the reader. + if (!write_all(fd, msg, strlen(msg))) { + break; + } + } + close(fd); + }); + writer.detach(); + + return httpserver::http_response::pipe(pipefd[0]) + .with_header("Content-Type", "text/plain"); + }); + + ws.start(true); + return 0; +} diff --git a/examples/service.cpp b/examples/service.cpp index 48ea39f93..9a83e26d1 100644 --- a/examples/service.cpp +++ b/examples/service.cpp @@ -18,225 +18,145 @@ USA */ -#include -#include #include -#include - -using namespace httpserver; - -bool verbose=false; -class service_resource: public http_resource { -public: - service_resource(); +#include +#include - ~service_resource(); - - const http_response render_GET(const http_request &req); - const http_response render_PUT(const http_request &req); - const http_response render_POST(const http_request &req); - const http_response render(const http_request &req); - const http_response render_HEAD(const http_request &req); - const http_response render_OPTIONS(const http_request &req); - const http_response render_CONNECT(const http_request &req); - const http_response render_DELETE(const http_request &req); +#include -private: +static bool verbose = false; +static void log_if_verbose(const httpserver::http_request& req, + const httpserver::http_response& res) { + if (verbose) { + std::cout << req; + std::cout << res; + } +} +class service_resource: public httpserver::http_resource { + public: + httpserver::http_response render_get(const httpserver::http_request &req) override; + httpserver::http_response render_put(const httpserver::http_request &req) override; + httpserver::http_response render_post(const httpserver::http_request &req) override; + httpserver::http_response render(const httpserver::http_request &req) override; + httpserver::http_response render_head(const httpserver::http_request &req) override; + httpserver::http_response render_options(const httpserver::http_request &req) override; + httpserver::http_response render_connect(const httpserver::http_request &req) override; + httpserver::http_response render_delete(const httpserver::http_request &req) override; }; -service_resource::service_resource() -{} - -service_resource::~service_resource() -{} - -const http_response -service_resource::render_GET(const http_request &req) -{ - std::cout << "service_resource::render_GET()" << std::endl; - - if (verbose) std::cout << req; - - http_response res(http_response_builder("GET response", 200).string_response()); - - if (verbose) std::cout << res; - +httpserver::http_response service_resource::render_get(const httpserver::http_request &req) { + std::cout << "service_resource::render_get()" << std::endl; + auto res = httpserver::http_response::string("GET response"); + log_if_verbose(req, res); return res; - } - -const http_response -service_resource::render_PUT(const http_request &req) -{ - std::cout << "service_resource::render_PUT()" << std::endl; - - if (verbose) std::cout << req; - - http_response res(http_response_builder("PUT response", 200).string_response()); - - if (verbose) std::cout << res; - +httpserver::http_response service_resource::render_put(const httpserver::http_request &req) { + std::cout << "service_resource::render_put()" << std::endl; + auto res = httpserver::http_response::string("PUT response"); + log_if_verbose(req, res); return res; } - -const http_response -service_resource::render_POST(const http_request &req) -{ - std::cout << "service_resource::render_POST()" << std::endl; - - if (verbose) std::cout << req; - - http_response res(http_response_builder("POST response", 200).string_response()); - - if (verbose) std::cout << res; - - return res; +httpserver::http_response service_resource::render_post(const httpserver::http_request &req) { + std::cout << "service_resource::render_post()" << std::endl; + auto res = httpserver::http_response::string("POST response"); + log_if_verbose(req, res); + return res; } -const http_response -service_resource::render(const http_request &req) -{ - std::cout << "service_resource::render()" << std::endl; - - if (verbose) std::cout << req; - - http_response res(http_response_builder("generic response", 200).string_response()); - - if (verbose) std::cout << res; - - return res; +httpserver::http_response service_resource::render(const httpserver::http_request &req) { + std::cout << "service_resource::render()" << std::endl; + auto res = httpserver::http_response::string("generic response"); + log_if_verbose(req, res); + return res; } - -const http_response -service_resource::render_HEAD(const http_request &req) -{ - std::cout << "service_resource::render_HEAD()" << std::endl; - - if (verbose) std::cout << req; - - http_response res(http_response_builder("HEAD response", 200).string_response()); - - if (verbose) std::cout << res; - - return res; +httpserver::http_response service_resource::render_head(const httpserver::http_request &req) { + std::cout << "service_resource::render_head()" << std::endl; + auto res = httpserver::http_response::string("HEAD response"); + log_if_verbose(req, res); + return res; } -const http_response -service_resource::render_OPTIONS(const http_request &req) -{ - std::cout << "service_resource::render_OPTIONS()" << std::endl; - - if (verbose) std::cout << req; - - http_response res(http_response_builder("OPTIONS response", 200).string_response()); - - if (verbose) std::cout << res; - - return res; +httpserver::http_response service_resource::render_options(const httpserver::http_request &req) { + std::cout << "service_resource::render_options()" << std::endl; + auto res = httpserver::http_response::string("OPTIONS response"); + log_if_verbose(req, res); + return res; } -const http_response -service_resource::render_CONNECT(const http_request &req) -{ - std::cout << "service_resource::render_CONNECT()" << std::endl; - - if (verbose) std::cout << req; - - http_response res(http_response_builder("CONNECT response", 200).string_response()); - - if (verbose) std::cout << res; - - return res; +httpserver::http_response service_resource::render_connect(const httpserver::http_request &req) { + std::cout << "service_resource::render_connect()" << std::endl; + auto res = httpserver::http_response::string("CONNECT response"); + log_if_verbose(req, res); + return res; } -const http_response -service_resource::render_DELETE(const http_request &req) -{ - std::cout << "service_resource::render_DELETE()" << std::endl; - - if (verbose) std::cout << req; - - http_response res(http_response_builder("DELETE response", 200).string_response()); - - if (verbose) std::cout << res; - - return res; +httpserver::http_response service_resource::render_delete(const httpserver::http_request &req) { + std::cout << "service_resource::render_delete()" << std::endl; + auto res = httpserver::http_response::string("DELETE response"); + log_if_verbose(req, res); + return res; } -void usage() -{ +void usage() { std::cout << "Usage:" << std::endl << "service [-p ][-s [-k ][-c ]][-v]" << std::endl; } -int main(int argc, char **argv) -{ - uint16_t port=8080; - int c; - const char *key="key.pem"; - const char *cert="cert.pem"; - bool secure=false; - - while ((c = getopt(argc,argv,"p:k:c:sv?")) != EOF) { - switch (c) { - case 'p': - port=strtoul(optarg,NULL,10); - break; +int main(int argc, char **argv) { + uint16_t port = 8080; + int c; + const char *key = "key.pem"; + const char *cert = "cert.pem"; + bool secure = false; + + while ((c = getopt(argc, argv, "p:k:c:sv?")) != EOF) { + switch (c) { + case 'p': + port = strtoul(optarg, nullptr, 10); + break; case 'k': key = optarg; break; case 'c': - cert=optarg; + cert = optarg; break; case 's': - secure=true; + secure = true; break; case 'v': - verbose=true; + verbose = true; break; - default: + default: usage(); exit(1); - break; - } - } + break; + } + } - std::cout << "Using port " << port << std::endl; + std::cout << "Using port " << port << std::endl; if (secure) { std::cout << "Key: " << key << " Certificate: " << cert << std::endl; } - // - // Use builder to define webserver configuration options - // - create_webserver cw = create_webserver(port).max_threads(5); + httpserver::create_webserver cw = httpserver::create_webserver(port).max_threads(5); if (secure) { cw.use_ssl().https_mem_key(key).https_mem_cert(cert); } - // - // Create webserver using the configured options - // - webserver ws = cw; + httpserver::webserver ws{cw}; - // - // Create and register service resource available at /service - // - service_resource res; - ws.register_resource("/service",&res,true); + auto res = std::make_shared(); + ws.register_prefix("/service", res); - // - // Start and block the webserver - // - ws.start(true); + ws.start(true); - return 0; + return 0; } diff --git a/examples/setting_headers.cpp b/examples/setting_headers.cpp new file mode 100644 index 000000000..0aefbc3cf --- /dev/null +++ b/examples/setting_headers.cpp @@ -0,0 +1,37 @@ +/* + This file is part of libhttpserver + Copyright (C) 2011-2025 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 +*/ + +// setting_headers.cpp - attach a response header to a lambda handler using +// the fluent with_header() setter, which is overloaded on both lvalue and +// rvalue http_response. + +#include + +int main() { + httpserver::webserver ws{httpserver::create_webserver(8080)}; + + ws.on_get("/hello", [](const httpserver::http_request&) { + return httpserver::http_response::string("Hello, World!") + .with_header("MyHeader", "MyValue"); + }); + + ws.start(true); + return 0; +} diff --git a/examples/shared_state.cpp b/examples/shared_state.cpp new file mode 100644 index 000000000..cdaa6fa3c --- /dev/null +++ b/examples/shared_state.cpp @@ -0,0 +1,63 @@ +/* + This file is part of libhttpserver + Copyright (C) 2011-2025 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 +*/ + +// shared_state.cpp - the canonical example of when the class form is the +// right shape. +// +// The lambda form (see hello_world.cpp) is the recommended idiom for +// stateless endpoints. When two HTTP methods on the same path share +// mutable state, however, two independent lambdas cannot safely capture +// the same data: each handler is invoked concurrently from libmicrohttpd +// worker threads, and there is no natural object lifetime that both +// lambdas can attach to. +// +// An http_resource subclass solves this cleanly. The resource owns the +// shared data, every render_* override is a member function with access +// to it, and a single std::mutex guards every mutation and read. + +#include +#include +#include + +#include + +class counter : public httpserver::http_resource { + public: + httpserver::http_response render_get(const httpserver::http_request&) override { + std::lock_guard lk(m_); + return httpserver::http_response::string(std::to_string(n_)); + } + + httpserver::http_response render_post(const httpserver::http_request&) override { + std::lock_guard lk(m_); + ++n_; + return httpserver::http_response::string(std::to_string(n_)).with_status(201); + } + + private: + std::mutex m_; + int n_ = 0; +}; + +int main() { + httpserver::webserver ws{httpserver::create_webserver(8080)}; + ws.register_path("/counter", std::make_unique()); + ws.start(true); +} diff --git a/examples/test_content b/examples/test_content new file mode 100755 index 000000000..5f643138e --- /dev/null +++ b/examples/test_content @@ -0,0 +1 @@ +test content of file diff --git a/examples/turbo_mode.cpp b/examples/turbo_mode.cpp new file mode 100644 index 000000000..fd12de243 --- /dev/null +++ b/examples/turbo_mode.cpp @@ -0,0 +1,41 @@ +/* + This file is part of libhttpserver + Copyright (C) 2011-2025 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 +*/ + +// turbo_mode.cpp - high-performance server configuration: turbo mode, +// suppressed Date header, fastopen queue and a tuned listen backlog. + +#include + +int main() { + httpserver::webserver ws{httpserver::create_webserver(8080) + .start_method(httpserver::http::http_utils::INTERNAL_SELECT) + .max_threads(4) + .turbo() + .suppress_date_header() + .tcp_fastopen_queue_size(16) + .listen_backlog(128)}; + + ws.on_get("/hello", [](const httpserver::http_request&) { + return httpserver::http_response::string("Hello, turbo world!"); + }); + + ws.start(true); + return 0; +} diff --git a/examples/url_registration.cpp b/examples/url_registration.cpp new file mode 100644 index 000000000..e55f419db --- /dev/null +++ b/examples/url_registration.cpp @@ -0,0 +1,80 @@ +/* + This file is part of libhttpserver + Copyright (C) 2011-2025 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 +*/ + +// url_registration.cpp - all four routing flavors: exact path, prefix +// match, plain regex, and parametric segments with optional constraints. +// +// The exact-path and parametric-arg endpoints register a stateless lambda +// directly. The prefix and regex endpoints share a small class because +// they want the same handler logic on multiple paths and on_* does not +// accept a precompiled-regex form for prefix matching. + +#include +#include + +#include + +class echo_path_resource : public httpserver::http_resource { + public: + httpserver::http_response render(const httpserver::http_request& req) override { + return httpserver::http_response::string( + "Your URL: " + std::string(req.get_path())); + } +}; + +int main() { + httpserver::webserver ws{httpserver::create_webserver(8080)}; + + // Exact path, stateless: the lambda form is the v2.0 recommended idiom. + ws.on_get("/hello", [](const httpserver::http_request&) { + return httpserver::http_response::string("Hello, World!"); + }); + + // Prefix-match and regex registration still go through the resource + // API. The same instance is registered against two routes. + auto echo = std::make_shared(); + ws.register_prefix("/family", echo); // /family and /family/* + ws.register_path("/with_regex_[0-9]+", echo); + + // Parametric segments with per-segment regex constraints (preferred form). + // The regex restricts arg1 to digits and arg2 to uppercase ASCII, so the + // router rejects invalid input before the handler is even invoked. + ws.on_get("/url/with/parametric/args/{arg1|[0-9]+}/and/{arg2|[A-Z]+}", + [](const httpserver::http_request& req) { + return httpserver::http_response::string( + "ARGS: " + std::string(req.get_arg("arg1")) + + " and " + std::string(req.get_arg("arg2"))); + }); + + // Unconstrained parametric segments (accepts any bytes in the URL slot). + // SECURITY NOTE: with no per-segment constraint the handler receives + // arbitrary input. Validate or sanitize before using in HTML, SQL, or + // system calls — raw reflection into an HTML response is reflected XSS + // (CWE-79); passing directly to a shell or DB is injection (CWE-89/78). + ws.on_get("/url/with/{arg1}/and/{arg2}", + [](const httpserver::http_request& req) { + return httpserver::http_response::string( + "ARGS: " + std::string(req.get_arg("arg1")) + + " and " + std::string(req.get_arg("arg2"))); + }); + + ws.start(true); + return 0; +} diff --git a/examples/websocket_echo.cpp b/examples/websocket_echo.cpp new file mode 100644 index 000000000..e085bace8 --- /dev/null +++ b/examples/websocket_echo.cpp @@ -0,0 +1,59 @@ +/* + 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 + +class echo_handler : public httpserver::websocket_handler { + public: + void on_open(httpserver::websocket_session& session) override { + std::cout << "WebSocket connection opened" << std::endl; + session.send_text("Welcome to the echo server!"); + } + + void on_message(httpserver::websocket_session& session, std::string_view msg) override { + std::cout << "Received: " << msg << std::endl; + std::string reply; + reply.reserve(6 + msg.size()); + reply = "Echo: "; + reply += msg; + session.send_text(reply); + } + + void on_close(httpserver::websocket_session& session, uint16_t code, const std::string& reason) override { + std::cout << "WebSocket closed (code=" << code << ", reason=" << reason << ")" << std::endl; + } +}; + +int main() { + httpserver::webserver ws{httpserver::create_webserver(8080)}; + + // TASK-035: smart-pointer ownership. The webserver takes ownership + // of the echo_handler via unique_ptr; its dtor runs when the + // webserver is destroyed. + ws.register_ws_resource("/ws", std::make_unique()); + ws.start(true); + + return 0; +} diff --git a/libhttpserver.pc.in b/libhttpserver.pc.in index 36355eeaf..305cc71e0 100644 --- a/libhttpserver.pc.in +++ b/libhttpserver.pc.in @@ -6,8 +6,8 @@ includedir=@includedir@ Name: libhttpserver Description: A C++ library for creating an embedded Rest HTTP server Version: @VERSION@ -Requires: libmicrohttpd >= 0.9.37 +Requires: libmicrohttpd >= 1.0.0 Conflicts: -Libs: -L${libdir} -lmicrohttpd +Libs: -L${libdir} -lhttpserver Libs.private: @LHT_LIBDEPS@ Cflags: -I${includedir} -I${includedir}/httpserver diff --git a/m4/ax_cxx_compile_stdcxx.m4 b/m4/ax_cxx_compile_stdcxx.m4 new file mode 100644 index 000000000..fe6ae17e6 --- /dev/null +++ b/m4/ax_cxx_compile_stdcxx.m4 @@ -0,0 +1,1070 @@ +# =========================================================================== +# https://www.gnu.org/software/autoconf-archive/ax_cxx_compile_stdcxx.html +# =========================================================================== +# +# SYNOPSIS +# +# AX_CXX_COMPILE_STDCXX(VERSION, [ext|noext], [mandatory|optional]) +# +# DESCRIPTION +# +# Check for baseline language coverage in the compiler for the specified +# version of the C++ standard. If necessary, add switches to CXX and +# CXXCPP to enable support. VERSION may be '11', '14', '17', '20', or +# '23' for the respective C++ standard version. +# +# The second argument, if specified, indicates whether you insist on an +# extended mode (e.g. -std=gnu++11) or a strict conformance mode (e.g. +# -std=c++11). If neither is specified, you get whatever works, with +# preference for no added switch, and then for an extended mode. +# +# The third argument, if specified 'mandatory' or if left unspecified, +# indicates that baseline support for the specified C++ standard is +# required and that the macro should error out if no mode with that +# support is found. If specified 'optional', then configuration proceeds +# regardless, after defining HAVE_CXX${VERSION} if and only if a +# supporting mode is found. +# +# LICENSE +# +# Copyright (c) 2008 Benjamin Kosnik +# Copyright (c) 2012 Zack Weinberg +# Copyright (c) 2013 Roy Stogner +# Copyright (c) 2014, 2015 Google Inc.; contributed by Alexey Sokolov +# Copyright (c) 2015 Paul Norman +# Copyright (c) 2015 Moritz Klammler +# Copyright (c) 2016, 2018 Krzesimir Nowak +# Copyright (c) 2019 Enji Cooper +# Copyright (c) 2020 Jason Merrill +# Copyright (c) 2021, 2024 Jörn Heusipp +# Copyright (c) 2015, 2022, 2023, 2024 Olly Betts +# +# 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 any +# warranty. + +#serial 25 + +dnl This macro is based on the code from the AX_CXX_COMPILE_STDCXX_11 macro +dnl (serial version number 13). + +AC_DEFUN([AX_CXX_COMPILE_STDCXX], [dnl + m4_if([$1], [11], [ax_cxx_compile_alternatives="11 0x"], + [$1], [14], [ax_cxx_compile_alternatives="14 1y"], + [$1], [17], [ax_cxx_compile_alternatives="17 1z"], + [$1], [20], [ax_cxx_compile_alternatives="20"], + [$1], [23], [ax_cxx_compile_alternatives="23"], + [m4_fatal([invalid first argument `$1' to AX_CXX_COMPILE_STDCXX])])dnl + m4_if([$2], [], [], + [$2], [ext], [], + [$2], [noext], [], + [m4_fatal([invalid second argument `$2' to AX_CXX_COMPILE_STDCXX])])dnl + m4_if([$3], [], [ax_cxx_compile_cxx$1_required=true], + [$3], [mandatory], [ax_cxx_compile_cxx$1_required=true], + [$3], [optional], [ax_cxx_compile_cxx$1_required=false], + [m4_fatal([invalid third argument `$3' to AX_CXX_COMPILE_STDCXX])]) + AC_LANG_PUSH([C++])dnl + ac_success=no + + m4_if([$2], [], [dnl + AC_CACHE_CHECK(whether $CXX supports C++$1 features by default, + ax_cv_cxx_compile_cxx$1, + [AC_COMPILE_IFELSE([AC_LANG_SOURCE([_AX_CXX_COMPILE_STDCXX_testbody_$1])], + [ax_cv_cxx_compile_cxx$1=yes], + [ax_cv_cxx_compile_cxx$1=no])]) + if test x$ax_cv_cxx_compile_cxx$1 = xyes; then + ac_success=yes + fi]) + + m4_if([$2], [noext], [], [dnl + if test x$ac_success = xno; then + for alternative in ${ax_cxx_compile_alternatives}; do + switch="-std=gnu++${alternative}" + cachevar=AS_TR_SH([ax_cv_cxx_compile_cxx$1_$switch]) + AC_CACHE_CHECK(whether $CXX supports C++$1 features with $switch, + $cachevar, + [ac_save_CXX="$CXX" + CXX="$CXX $switch" + AC_COMPILE_IFELSE([AC_LANG_SOURCE([_AX_CXX_COMPILE_STDCXX_testbody_$1])], + [eval $cachevar=yes], + [eval $cachevar=no]) + CXX="$ac_save_CXX"]) + if eval test x\$$cachevar = xyes; then + CXX="$CXX $switch" + if test -n "$CXXCPP" ; then + CXXCPP="$CXXCPP $switch" + fi + ac_success=yes + break + fi + done + fi]) + + m4_if([$2], [ext], [], [dnl + if test x$ac_success = xno; then + dnl HP's aCC needs +std=c++11 according to: + dnl http://h21007.www2.hp.com/portal/download/files/unprot/aCxx/PDF_Release_Notes/769149-001.pdf + dnl Cray's crayCC needs "-h std=c++11" + dnl MSVC needs -std:c++NN for C++17 and later (default is C++14) + for alternative in ${ax_cxx_compile_alternatives}; do + for switch in -std=c++${alternative} +std=c++${alternative} "-h std=c++${alternative}" MSVC; do + if test x"$switch" = xMSVC; then + dnl AS_TR_SH maps both `:` and `=` to `_` so -std:c++17 would collide + dnl with -std=c++17. We suffix the cache variable name with _MSVC to + dnl avoid this. + switch=-std:c++${alternative} + cachevar=AS_TR_SH([ax_cv_cxx_compile_cxx$1_${switch}_MSVC]) + else + cachevar=AS_TR_SH([ax_cv_cxx_compile_cxx$1_$switch]) + fi + AC_CACHE_CHECK(whether $CXX supports C++$1 features with $switch, + $cachevar, + [ac_save_CXX="$CXX" + CXX="$CXX $switch" + AC_COMPILE_IFELSE([AC_LANG_SOURCE([_AX_CXX_COMPILE_STDCXX_testbody_$1])], + [eval $cachevar=yes], + [eval $cachevar=no]) + CXX="$ac_save_CXX"]) + if eval test x\$$cachevar = xyes; then + CXX="$CXX $switch" + if test -n "$CXXCPP" ; then + CXXCPP="$CXXCPP $switch" + fi + ac_success=yes + break + fi + done + if test x$ac_success = xyes; then + break + fi + done + fi]) + AC_LANG_POP([C++]) + if test x$ax_cxx_compile_cxx$1_required = xtrue; then + if test x$ac_success = xno; then + AC_MSG_ERROR([*** A compiler with support for C++$1 language features is required.]) + fi + fi + if test x$ac_success = xno; then + HAVE_CXX$1=0 + AC_MSG_NOTICE([No compiler with C++$1 support was found]) + else + HAVE_CXX$1=1 + AC_DEFINE(HAVE_CXX$1,1, + [define if the compiler supports basic C++$1 syntax]) + fi + AC_SUBST(HAVE_CXX$1) +]) + + +dnl Test body for checking C++11 support + +m4_define([_AX_CXX_COMPILE_STDCXX_testbody_11], + [_AX_CXX_COMPILE_STDCXX_testbody_new_in_11] +) + +dnl Test body for checking C++14 support + +m4_define([_AX_CXX_COMPILE_STDCXX_testbody_14], + [_AX_CXX_COMPILE_STDCXX_testbody_new_in_11 + _AX_CXX_COMPILE_STDCXX_testbody_new_in_14] +) + +dnl Test body for checking C++17 support + +m4_define([_AX_CXX_COMPILE_STDCXX_testbody_17], + [_AX_CXX_COMPILE_STDCXX_testbody_new_in_11 + _AX_CXX_COMPILE_STDCXX_testbody_new_in_14 + _AX_CXX_COMPILE_STDCXX_testbody_new_in_17] +) + +dnl Test body for checking C++20 support + +m4_define([_AX_CXX_COMPILE_STDCXX_testbody_20], + [_AX_CXX_COMPILE_STDCXX_testbody_new_in_11 + _AX_CXX_COMPILE_STDCXX_testbody_new_in_14 + _AX_CXX_COMPILE_STDCXX_testbody_new_in_17 + _AX_CXX_COMPILE_STDCXX_testbody_new_in_20] +) + +dnl Test body for checking C++23 support + +m4_define([_AX_CXX_COMPILE_STDCXX_testbody_23], + [_AX_CXX_COMPILE_STDCXX_testbody_new_in_11 + _AX_CXX_COMPILE_STDCXX_testbody_new_in_14 + _AX_CXX_COMPILE_STDCXX_testbody_new_in_17 + _AX_CXX_COMPILE_STDCXX_testbody_new_in_20 + _AX_CXX_COMPILE_STDCXX_testbody_new_in_23] +) + + +dnl Tests for new features in C++11 + +m4_define([_AX_CXX_COMPILE_STDCXX_testbody_new_in_11], [[ + +// If the compiler admits that it is not ready for C++11, why torture it? +// Hopefully, this will speed up the test. + +#ifndef __cplusplus + +#error "This is not a C++ compiler" + +// MSVC always sets __cplusplus to 199711L in older versions; newer versions +// only set it correctly if /Zc:__cplusplus is specified as well as a +// /std:c++NN switch: +// +// https://devblogs.microsoft.com/cppblog/msvc-now-correctly-reports-__cplusplus/ +// +// The value __cplusplus ought to have is available in _MSVC_LANG since +// Visual Studio 2015 Update 3: +// +// https://learn.microsoft.com/en-us/cpp/preprocessor/predefined-macros +// +// This was also the first MSVC version to support C++14 so we can't use the +// value of either __cplusplus or _MSVC_LANG to quickly rule out MSVC having +// C++11 or C++14 support, but we can check _MSVC_LANG for C++17 and later. +#elif __cplusplus < 201103L && !defined _MSC_VER + +#error "This is not a C++11 compiler" + +#else + +namespace cxx11 +{ + + namespace test_static_assert + { + + template + struct check + { + static_assert(sizeof(int) <= sizeof(T), "not big enough"); + }; + + } + + namespace test_final_override + { + + struct Base + { + virtual ~Base() {} + virtual void f() {} + }; + + struct Derived : public Base + { + virtual ~Derived() override {} + virtual void f() override {} + }; + + } + + namespace test_double_right_angle_brackets + { + + template < typename T > + struct check {}; + + typedef check single_type; + typedef check> double_type; + typedef check>> triple_type; + typedef check>>> quadruple_type; + + } + + namespace test_decltype + { + + int + f() + { + int a = 1; + decltype(a) b = 2; + return a + b; + } + + } + + namespace test_type_deduction + { + + template < typename T1, typename T2 > + struct is_same + { + static const bool value = false; + }; + + template < typename T > + struct is_same + { + static const bool value = true; + }; + + template < typename T1, typename T2 > + auto + add(T1 a1, T2 a2) -> decltype(a1 + a2) + { + return a1 + a2; + } + + int + test(const int c, volatile int v) + { + static_assert(is_same::value == true, ""); + static_assert(is_same::value == false, ""); + static_assert(is_same::value == false, ""); + auto ac = c; + auto av = v; + auto sumi = ac + av + 'x'; + auto sumf = ac + av + 1.0; + static_assert(is_same::value == true, ""); + static_assert(is_same::value == true, ""); + static_assert(is_same::value == true, ""); + static_assert(is_same::value == false, ""); + static_assert(is_same::value == true, ""); + return (sumf > 0.0) ? sumi : add(c, v); + } + + } + + namespace test_noexcept + { + + int f() { return 0; } + int g() noexcept { return 0; } + + static_assert(noexcept(f()) == false, ""); + static_assert(noexcept(g()) == true, ""); + + } + + namespace test_constexpr + { + + template < typename CharT > + unsigned long constexpr + strlen_c_r(const CharT *const s, const unsigned long acc) noexcept + { + return *s ? strlen_c_r(s + 1, acc + 1) : acc; + } + + template < typename CharT > + unsigned long constexpr + strlen_c(const CharT *const s) noexcept + { + return strlen_c_r(s, 0UL); + } + + static_assert(strlen_c("") == 0UL, ""); + static_assert(strlen_c("1") == 1UL, ""); + static_assert(strlen_c("example") == 7UL, ""); + static_assert(strlen_c("another\0example") == 7UL, ""); + + } + + namespace test_rvalue_references + { + + template < int N > + struct answer + { + static constexpr int value = N; + }; + + answer<1> f(int&) { return answer<1>(); } + answer<2> f(const int&) { return answer<2>(); } + answer<3> f(int&&) { return answer<3>(); } + + void + test() + { + int i = 0; + const int c = 0; + static_assert(decltype(f(i))::value == 1, ""); + static_assert(decltype(f(c))::value == 2, ""); + static_assert(decltype(f(0))::value == 3, ""); + } + + } + + namespace test_uniform_initialization + { + + struct test + { + static const int zero {}; + static const int one {1}; + }; + + static_assert(test::zero == 0, ""); + static_assert(test::one == 1, ""); + + } + + namespace test_lambdas + { + + void + test1() + { + auto lambda1 = [](){}; + auto lambda2 = lambda1; + lambda1(); + lambda2(); + } + + int + test2() + { + auto a = [](int i, int j){ return i + j; }(1, 2); + auto b = []() -> int { return '0'; }(); + auto c = [=](){ return a + b; }(); + auto d = [&](){ return c; }(); + auto e = [a, &b](int x) mutable { + const auto identity = [](int y){ return y; }; + for (auto i = 0; i < a; ++i) + a += b--; + return x + identity(a + b); + }(0); + return a + b + c + d + e; + } + + int + test3() + { + const auto nullary = [](){ return 0; }; + const auto unary = [](int x){ return x; }; + using nullary_t = decltype(nullary); + using unary_t = decltype(unary); + const auto higher1st = [](nullary_t f){ return f(); }; + const auto higher2nd = [unary](nullary_t f1){ + return [unary, f1](unary_t f2){ return f2(unary(f1())); }; + }; + return higher1st(nullary) + higher2nd(nullary)(unary); + } + + } + + namespace test_variadic_templates + { + + template + struct sum; + + template + struct sum + { + static constexpr auto value = N0 + sum::value; + }; + + template <> + struct sum<> + { + static constexpr auto value = 0; + }; + + static_assert(sum<>::value == 0, ""); + static_assert(sum<1>::value == 1, ""); + static_assert(sum<23>::value == 23, ""); + static_assert(sum<1, 2>::value == 3, ""); + static_assert(sum<5, 5, 11>::value == 21, ""); + static_assert(sum<2, 3, 5, 7, 11, 13>::value == 41, ""); + + } + + // http://stackoverflow.com/questions/13728184/template-aliases-and-sfinae + // Clang 3.1 fails with headers of libstd++ 4.8.3 when using std::function + // because of this. + namespace test_template_alias_sfinae + { + + struct foo {}; + + template + using member = typename T::member_type; + + template + void func(...) {} + + template + void func(member*) {} + + void test(); + + void test() { func(0); } + + } + +} // namespace cxx11 + +#endif // __cplusplus >= 201103L + +]]) + + +dnl Tests for new features in C++14 + +m4_define([_AX_CXX_COMPILE_STDCXX_testbody_new_in_14], [[ + +// If the compiler admits that it is not ready for C++14, why torture it? +// Hopefully, this will speed up the test. + +#ifndef __cplusplus + +#error "This is not a C++ compiler" + +#elif __cplusplus < 201402L && !defined _MSC_VER + +#error "This is not a C++14 compiler" + +#else + +namespace cxx14 +{ + + namespace test_polymorphic_lambdas + { + + int + test() + { + const auto lambda = [](auto&&... args){ + const auto istiny = [](auto x){ + return (sizeof(x) == 1UL) ? 1 : 0; + }; + const int aretiny[] = { istiny(args)... }; + return aretiny[0]; + }; + return lambda(1, 1L, 1.0f, '1'); + } + + } + + namespace test_binary_literals + { + + constexpr auto ivii = 0b0000000000101010; + static_assert(ivii == 42, "wrong value"); + + } + + namespace test_generalized_constexpr + { + + template < typename CharT > + constexpr unsigned long + strlen_c(const CharT *const s) noexcept + { + auto length = 0UL; + for (auto p = s; *p; ++p) + ++length; + return length; + } + + static_assert(strlen_c("") == 0UL, ""); + static_assert(strlen_c("x") == 1UL, ""); + static_assert(strlen_c("test") == 4UL, ""); + static_assert(strlen_c("another\0test") == 7UL, ""); + + } + + namespace test_lambda_init_capture + { + + int + test() + { + auto x = 0; + const auto lambda1 = [a = x](int b){ return a + b; }; + const auto lambda2 = [a = lambda1(x)](){ return a; }; + return lambda2(); + } + + } + + namespace test_digit_separators + { + + constexpr auto ten_million = 100'000'000; + static_assert(ten_million == 100000000, ""); + + } + + namespace test_return_type_deduction + { + + auto f(int& x) { return x; } + decltype(auto) g(int& x) { return x; } + + template < typename T1, typename T2 > + struct is_same + { + static constexpr auto value = false; + }; + + template < typename T > + struct is_same + { + static constexpr auto value = true; + }; + + int + test() + { + auto x = 0; + static_assert(is_same::value, ""); + static_assert(is_same::value, ""); + return x; + } + + } + +} // namespace cxx14 + +#endif // __cplusplus >= 201402L + +]]) + + +dnl Tests for new features in C++17 + +m4_define([_AX_CXX_COMPILE_STDCXX_testbody_new_in_17], [[ + +// If the compiler admits that it is not ready for C++17, why torture it? +// Hopefully, this will speed up the test. + +#ifndef __cplusplus + +#error "This is not a C++ compiler" + +#elif (defined _MSVC_LANG ? _MSVC_LANG : __cplusplus) < 201703L + +#error "This is not a C++17 compiler" + +#else + +#include +#include +#include + +namespace cxx17 +{ + + namespace test_constexpr_lambdas + { + + constexpr int foo = [](){return 42;}(); + + } + + namespace test::nested_namespace::definitions + { + + } + + namespace test_fold_expression + { + + template + int multiply(Args... args) + { + return (args * ... * 1); + } + + template + bool all(Args... args) + { + return (args && ...); + } + + } + + namespace test_extended_static_assert + { + + static_assert (true); + + } + + namespace test_auto_brace_init_list + { + + auto foo = {5}; + auto bar {5}; + + static_assert(std::is_same, decltype(foo)>::value); + static_assert(std::is_same::value); + } + + namespace test_typename_in_template_template_parameter + { + + template typename X> struct D; + + } + + namespace test_fallthrough_nodiscard_maybe_unused_attributes + { + + int f1() + { + return 42; + } + + [[nodiscard]] int f2() + { + [[maybe_unused]] auto unused = f1(); + + switch (f1()) + { + case 17: + f1(); + [[fallthrough]]; + case 42: + f1(); + } + return f1(); + } + + } + + namespace test_extended_aggregate_initialization + { + + struct base1 + { + int b1, b2 = 42; + }; + + struct base2 + { + base2() { + b3 = 42; + } + int b3; + }; + + struct derived : base1, base2 + { + int d; + }; + + derived d1 {{1, 2}, {}, 4}; // full initialization + derived d2 {{}, {}, 4}; // value-initialized bases + + } + + namespace test_general_range_based_for_loop + { + + struct iter + { + int i; + + int& operator* () + { + return i; + } + + const int& operator* () const + { + return i; + } + + iter& operator++() + { + ++i; + return *this; + } + }; + + struct sentinel + { + int i; + }; + + bool operator== (const iter& i, const sentinel& s) + { + return i.i == s.i; + } + + bool operator!= (const iter& i, const sentinel& s) + { + return !(i == s); + } + + struct range + { + iter begin() const + { + return {0}; + } + + sentinel end() const + { + return {5}; + } + }; + + void f() + { + range r {}; + + for (auto i : r) + { + [[maybe_unused]] auto v = i; + } + } + + } + + namespace test_lambda_capture_asterisk_this_by_value + { + + struct t + { + int i; + int foo() + { + return [*this]() + { + return i; + }(); + } + }; + + } + + namespace test_enum_class_construction + { + + enum class byte : unsigned char + {}; + + byte foo {42}; + + } + + namespace test_constexpr_if + { + + template + int f () + { + if constexpr(cond) + { + return 13; + } + else + { + return 42; + } + } + + } + + namespace test_selection_statement_with_initializer + { + + int f() + { + return 13; + } + + int f2() + { + if (auto i = f(); i > 0) + { + return 3; + } + + switch (auto i = f(); i + 4) + { + case 17: + return 2; + + default: + return 1; + } + } + + } + + namespace test_template_argument_deduction_for_class_templates + { + + template + struct pair + { + pair (T1 p1, T2 p2) + : m1 {p1}, + m2 {p2} + {} + + T1 m1; + T2 m2; + }; + + void f() + { + [[maybe_unused]] auto p = pair{13, 42u}; + } + + } + + namespace test_non_type_auto_template_parameters + { + + template + struct B + {}; + + B<5> b1; + B<'a'> b2; + + } + + namespace test_structured_bindings + { + + int arr[2] = { 1, 2 }; + std::pair pr = { 1, 2 }; + + auto f1() -> int(&)[2] + { + return arr; + } + + auto f2() -> std::pair& + { + return pr; + } + + struct S + { + int x1 : 2; + volatile double y1; + }; + + S f3() + { + return {}; + } + + auto [ x1, y1 ] = f1(); + auto& [ xr1, yr1 ] = f1(); + auto [ x2, y2 ] = f2(); + auto& [ xr2, yr2 ] = f2(); + const auto [ x3, y3 ] = f3(); + + } + + namespace test_exception_spec_type_system + { + + struct Good {}; + struct Bad {}; + + void g1() noexcept; + void g2(); + + template + Bad + f(T*, T*); + + template + Good + f(T1*, T2*); + + static_assert (std::is_same_v); + + } + + namespace test_inline_variables + { + + template void f(T) + {} + + template inline T g(T) + { + return T{}; + } + + template<> inline void f<>(int) + {} + + template<> int g<>(int) + { + return 5; + } + + } + +} // namespace cxx17 + +#endif // (defined _MSVC_LANG ? _MSVC_LANG : __cplusplus) < 201703L + +]]) + + +dnl Tests for new features in C++20 + +m4_define([_AX_CXX_COMPILE_STDCXX_testbody_new_in_20], [[ + +#ifndef __cplusplus + +#error "This is not a C++ compiler" + +#elif (defined _MSVC_LANG ? _MSVC_LANG : __cplusplus) < 202002L + +#error "This is not a C++20 compiler" + +#else + +#include + +namespace cxx20 +{ + +// As C++20 supports feature test macros in the standard, there is no +// immediate need to actually test for feature availability on the +// Autoconf side. + +} // namespace cxx20 + +#endif // (defined _MSVC_LANG ? _MSVC_LANG : __cplusplus) < 202002L + +]]) + + +dnl Tests for new features in C++23 + +m4_define([_AX_CXX_COMPILE_STDCXX_testbody_new_in_23], [[ + +#ifndef __cplusplus + +#error "This is not a C++ compiler" + +#elif (defined _MSVC_LANG ? _MSVC_LANG : __cplusplus) < 202302L + +#error "This is not a C++23 compiler" + +#else + +#include + +namespace cxx23 +{ + +// As C++23 supports feature test macros in the standard, there is no +// immediate need to actually test for feature availability on the +// Autoconf side. + +} // namespace cxx23 + +#endif // (defined _MSVC_LANG ? _MSVC_LANG : __cplusplus) < 202302L + +]]) diff --git a/m4/ax_have_epoll.m4 b/m4/ax_have_epoll.m4 new file mode 100644 index 000000000..9d9bc8736 --- /dev/null +++ b/m4/ax_have_epoll.m4 @@ -0,0 +1,104 @@ +# =========================================================================== +# https://www.gnu.org/software/autoconf-archive/ax_have_epoll.html +# =========================================================================== +# +# SYNOPSIS +# +# AX_HAVE_EPOLL([ACTION-IF-FOUND], [ACTION-IF-NOT-FOUND]) +# AX_HAVE_EPOLL_PWAIT([ACTION-IF-FOUND], [ACTION-IF-NOT-FOUND]) +# +# DESCRIPTION +# +# This macro determines whether the system supports the epoll I/O event +# interface. A neat usage example would be: +# +# AX_HAVE_EPOLL( +# [AX_CONFIG_FEATURE_ENABLE(epoll)], +# [AX_CONFIG_FEATURE_DISABLE(epoll)]) +# AX_CONFIG_FEATURE( +# [epoll], [This platform supports epoll(7)], +# [HAVE_EPOLL], [This platform supports epoll(7).]) +# +# The epoll interface was added to the Linux kernel in version 2.5.45, and +# the macro verifies that a kernel newer than this is installed. This +# check is somewhat unreliable if doesn't match the +# running kernel, but it is necessary regardless, because glibc comes with +# stubs for the epoll_create(), epoll_wait(), etc. that allow programs to +# compile and link even if the kernel is too old; the problem would then +# be detected only at runtime. +# +# Linux kernel version 2.6.19 adds the epoll_pwait() call in addition to +# epoll_wait(). The availability of that function can be tested with the +# second macro. Generally speaking, it is safe to assume that +# AX_HAVE_EPOLL would succeed if AX_HAVE_EPOLL_PWAIT has, but not the +# other way round. +# +# LICENSE +# +# Copyright (c) 2008 Peter Simons +# +# 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 any +# warranty. + +#serial 11 + +AC_DEFUN([AX_HAVE_EPOLL], [dnl + ax_have_epoll_cppflags="${CPPFLAGS}" + AC_CHECK_HEADER([linux/version.h], [CPPFLAGS="${CPPFLAGS} -DHAVE_LINUX_VERSION_H"]) + AC_MSG_CHECKING([for Linux epoll(7) interface]) + AC_CACHE_VAL([ax_cv_have_epoll], [dnl + AC_LINK_IFELSE([dnl + AC_LANG_PROGRAM([dnl +#include +#ifdef HAVE_LINUX_VERSION_H +# include +# if LINUX_VERSION_CODE < KERNEL_VERSION(2,5,45) +# error linux kernel version is too old to have epoll +# endif +#endif +], [dnl +int fd, rc; +struct epoll_event ev; +fd = epoll_create(128); +rc = epoll_wait(fd, &ev, 1, 0);])], + [ax_cv_have_epoll=yes], + [ax_cv_have_epoll=no])]) + CPPFLAGS="${ax_have_epoll_cppflags}" + AS_IF([test "${ax_cv_have_epoll}" = "yes"], + [AC_MSG_RESULT([yes]) +$1],[AC_MSG_RESULT([no]) +$2]) +])dnl + +AC_DEFUN([AX_HAVE_EPOLL_PWAIT], [dnl + ax_have_epoll_cppflags="${CPPFLAGS}" + AC_CHECK_HEADER([linux/version.h], + [CPPFLAGS="${CPPFLAGS} -DHAVE_LINUX_VERSION_H"]) + AC_MSG_CHECKING([for Linux epoll(7) interface with signals extension]) + AC_CACHE_VAL([ax_cv_have_epoll_pwait], [dnl + AC_LINK_IFELSE([dnl + AC_LANG_PROGRAM([dnl +#ifdef HAVE_LINUX_VERSION_H +# include +# if LINUX_VERSION_CODE < KERNEL_VERSION(2,6,19) +# error linux kernel version is too old to have epoll_pwait +# endif +#endif +#include +#include +], [dnl +int fd, rc; +struct epoll_event ev; +fd = epoll_create(128); +rc = epoll_wait(fd, &ev, 1, 0); +rc = epoll_pwait(fd, &ev, 1, 0, (sigset_t const *)(0));])], + [ax_cv_have_epoll_pwait=yes], + [ax_cv_have_epoll_pwait=no])]) + CPPFLAGS="${ax_have_epoll_cppflags}" + AS_IF([test "${ax_cv_have_epoll_pwait}" = "yes"], + [AC_MSG_RESULT([yes]) +$1],[AC_MSG_RESULT([no]) +$2]) +])dnl diff --git a/m4/ax_valgrind_check.m4 b/m4/ax_valgrind_check.m4 new file mode 100644 index 000000000..70337981f --- /dev/null +++ b/m4/ax_valgrind_check.m4 @@ -0,0 +1,239 @@ +# =========================================================================== +# https://www.gnu.org/software/autoconf-archive/ax_valgrind_check.html +# =========================================================================== +# +# SYNOPSIS +# +# AX_VALGRIND_DFLT(memcheck|helgrind|drd|sgcheck, on|off) +# AX_VALGRIND_CHECK() +# +# DESCRIPTION +# +# AX_VALGRIND_CHECK checks whether Valgrind is present and, if so, allows +# running `make check` under a variety of Valgrind tools to check for +# memory and threading errors. +# +# Defines VALGRIND_CHECK_RULES which should be substituted in your +# Makefile; and $enable_valgrind which can be used in subsequent configure +# output. VALGRIND_ENABLED is defined and substituted, and corresponds to +# the value of the --enable-valgrind option, which defaults to being +# enabled if Valgrind is installed and disabled otherwise. Individual +# Valgrind tools can be disabled via --disable-valgrind-, the +# default is configurable via the AX_VALGRIND_DFLT command or is to use +# all commands not disabled via AX_VALGRIND_DFLT. All AX_VALGRIND_DFLT +# calls must be made before the call to AX_VALGRIND_CHECK. +# +# If unit tests are written using a shell script and automake's +# LOG_COMPILER system, the $(VALGRIND) variable can be used within the +# shell scripts to enable Valgrind, as described here: +# +# https://www.gnu.org/software/gnulib/manual/html_node/Running-self_002dtests-under-valgrind.html +# +# Usage example: +# +# configure.ac: +# +# AX_VALGRIND_DFLT([sgcheck], [off]) +# AX_VALGRIND_CHECK +# +# in each Makefile.am with tests: +# +# @VALGRIND_CHECK_RULES@ +# VALGRIND_SUPPRESSIONS_FILES = my-project.supp +# EXTRA_DIST = my-project.supp +# +# This results in a "check-valgrind" rule being added. Running `make +# check-valgrind` in that directory will recursively run the module's test +# suite (`make check`) once for each of the available Valgrind tools (out +# of memcheck, helgrind and drd) while the sgcheck will be skipped unless +# enabled again on the commandline with --enable-valgrind-sgcheck. The +# results for each check will be output to test-suite-$toolname.log. The +# target will succeed if there are zero errors and fail otherwise. +# +# Alternatively, a "check-valgrind-$TOOL" rule will be added, for $TOOL in +# memcheck, helgrind, drd and sgcheck. These are useful because often only +# some of those tools can be ran cleanly on a codebase. +# +# The macro supports running with and without libtool. +# +# LICENSE +# +# Copyright (c) 2014, 2015, 2016 Philip Withnall +# +# 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 any +# warranty. + +#serial 17 + +dnl Configured tools +m4_define([valgrind_tool_list], [[memcheck], [helgrind], [drd], [sgcheck]]) +m4_set_add_all([valgrind_exp_tool_set], [sgcheck]) +m4_foreach([vgtool], [valgrind_tool_list], + [m4_define([en_dflt_valgrind_]vgtool, [on])]) + +AC_DEFUN([AX_VALGRIND_DFLT],[ + m4_define([en_dflt_valgrind_$1], [$2]) +])dnl + +AM_EXTRA_RECURSIVE_TARGETS([check-valgrind]) +m4_foreach([vgtool], [valgrind_tool_list], + [AM_EXTRA_RECURSIVE_TARGETS([check-valgrind-]vgtool)]) + +AC_DEFUN([AX_VALGRIND_CHECK],[ + dnl Check for --enable-valgrind + AC_ARG_ENABLE([valgrind], + [AS_HELP_STRING([--enable-valgrind], [Whether to enable Valgrind on the unit tests])], + [enable_valgrind=$enableval],[enable_valgrind=]) + + AS_IF([test "$enable_valgrind" != "no"],[ + # Check for Valgrind. + AC_CHECK_PROG([VALGRIND],[valgrind],[valgrind]) + AS_IF([test "$VALGRIND" = ""],[ + AS_IF([test "$enable_valgrind" = "yes"],[ + AC_MSG_ERROR([Could not find valgrind; either install it or reconfigure with --disable-valgrind]) + ],[ + enable_valgrind=no + ]) + ],[ + enable_valgrind=yes + ]) + ]) + + AM_CONDITIONAL([VALGRIND_ENABLED],[test "$enable_valgrind" = "yes"]) + AC_SUBST([VALGRIND_ENABLED],[$enable_valgrind]) + + # Check for Valgrind tools we care about. + [valgrind_enabled_tools=] + m4_foreach([vgtool],[valgrind_tool_list],[ + AC_ARG_ENABLE([valgrind-]vgtool, + m4_if(m4_defn([en_dflt_valgrind_]vgtool),[off],dnl +[AS_HELP_STRING([--enable-valgrind-]vgtool, [Whether to use ]vgtool[ during the Valgrind tests])],dnl +[AS_HELP_STRING([--disable-valgrind-]vgtool, [Whether to skip ]vgtool[ during the Valgrind tests])]), + [enable_valgrind_]vgtool[=$enableval], + [enable_valgrind_]vgtool[=]) + AS_IF([test "$enable_valgrind" = "no"],[ + enable_valgrind_]vgtool[=no], + [test "$enable_valgrind_]vgtool[" ]dnl +m4_if(m4_defn([en_dflt_valgrind_]vgtool), [off], [= "yes"], [!= "no"]),[ + AC_CACHE_CHECK([for Valgrind tool ]vgtool, + [ax_cv_valgrind_tool_]vgtool,[ + ax_cv_valgrind_tool_]vgtool[=no + m4_set_contains([valgrind_exp_tool_set],vgtool, + [m4_define([vgtoolx],[exp-]vgtool)], + [m4_define([vgtoolx],vgtool)]) + AS_IF([`$VALGRIND --tool=]vgtoolx[ --help >/dev/null 2>&1`],[ + ax_cv_valgrind_tool_]vgtool[=yes + ]) + ]) + AS_IF([test "$ax_cv_valgrind_tool_]vgtool[" = "no"],[ + AS_IF([test "$enable_valgrind_]vgtool[" = "yes"],[ + AC_MSG_ERROR([Valgrind does not support ]vgtool[; reconfigure with --disable-valgrind-]vgtool) + ],[ + enable_valgrind_]vgtool[=no + ]) + ],[ + enable_valgrind_]vgtool[=yes + ]) + ]) + AS_IF([test "$enable_valgrind_]vgtool[" = "yes"],[ + valgrind_enabled_tools="$valgrind_enabled_tools ]m4_bpatsubst(vgtool,[^exp-])[" + ]) + AC_SUBST([ENABLE_VALGRIND_]vgtool,[$enable_valgrind_]vgtool) + ]) + AC_SUBST([valgrind_tools],["]m4_join([ ], valgrind_tool_list)["]) + AC_SUBST([valgrind_enabled_tools],[$valgrind_enabled_tools]) + +[VALGRIND_CHECK_RULES=' +# Valgrind check +# +# Optional: +# - VALGRIND_SUPPRESSIONS_FILES: Space-separated list of Valgrind suppressions +# files to load. (Default: empty) +# - VALGRIND_FLAGS: General flags to pass to all Valgrind tools. +# (Default: --num-callers=30) +# - VALGRIND_$toolname_FLAGS: Flags to pass to Valgrind $toolname (one of: +# memcheck, helgrind, drd, sgcheck). (Default: various) + +# Optional variables +VALGRIND_SUPPRESSIONS ?= $(addprefix --suppressions=,$(VALGRIND_SUPPRESSIONS_FILES)) +VALGRIND_FLAGS ?= --num-callers=30 +VALGRIND_memcheck_FLAGS ?= --leak-check=full --show-reachable=no +VALGRIND_helgrind_FLAGS ?= --history-level=approx +VALGRIND_drd_FLAGS ?= +VALGRIND_sgcheck_FLAGS ?= + +# Internal use +valgrind_log_files = $(addprefix test-suite-,$(addsuffix .log,$(valgrind_tools))) + +valgrind_memcheck_flags = --tool=memcheck $(VALGRIND_memcheck_FLAGS) +valgrind_helgrind_flags = --tool=helgrind $(VALGRIND_helgrind_FLAGS) +valgrind_drd_flags = --tool=drd $(VALGRIND_drd_FLAGS) +valgrind_sgcheck_flags = --tool=exp-sgcheck $(VALGRIND_sgcheck_FLAGS) + +valgrind_quiet = $(valgrind_quiet_$(V)) +valgrind_quiet_ = $(valgrind_quiet_$(AM_DEFAULT_VERBOSITY)) +valgrind_quiet_0 = --quiet +valgrind_v_use = $(valgrind_v_use_$(V)) +valgrind_v_use_ = $(valgrind_v_use_$(AM_DEFAULT_VERBOSITY)) +valgrind_v_use_0 = @echo " USE " $(patsubst check-valgrind-%-am,%,$''@):; + +# Support running with and without libtool. +ifneq ($(LIBTOOL),) +valgrind_lt = $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=execute +else +valgrind_lt = +endif + +# Use recursive makes in order to ignore errors during check +check-valgrind-am: +ifeq ($(VALGRIND_ENABLED),yes) + $(A''M_V_at)$(MAKE) $(AM_MAKEFLAGS) -k \ + $(foreach tool, $(valgrind_enabled_tools), check-valgrind-$(tool)) +else + @echo "Need to reconfigure with --enable-valgrind" +endif + +# Valgrind running +VALGRIND_TESTS_ENVIRONMENT = \ + $(TESTS_ENVIRONMENT) \ + env VALGRIND=$(VALGRIND) \ + G_SLICE=always-malloc,debug-blocks \ + G_DEBUG=fatal-warnings,fatal-criticals,gc-friendly + +VALGRIND_LOG_COMPILER = \ + $(valgrind_lt) \ + $(VALGRIND) $(VALGRIND_SUPPRESSIONS) --error-exitcode=1 $(VALGRIND_FLAGS) + +define valgrind_tool_rule +check-valgrind-$(1)-am: +ifeq ($$(VALGRIND_ENABLED)-$$(ENABLE_VALGRIND_$(1)),yes-yes) +ifneq ($$(TESTS),) + $$(valgrind_v_use)$$(MAKE) check-TESTS \ + TESTS_ENVIRONMENT="$$(VALGRIND_TESTS_ENVIRONMENT)" \ + LOG_COMPILER="$$(VALGRIND_LOG_COMPILER)" \ + LOG_FLAGS="$$(valgrind_$(1)_flags)" \ + TEST_SUITE_LOG=test-suite-$(1).log +endif +else ifeq ($$(VALGRIND_ENABLED),yes) + @echo "Need to reconfigure with --enable-valgrind-$(1)" +else + @echo "Need to reconfigure with --enable-valgrind" +endif +endef + +$(foreach tool,$(valgrind_tools),$(eval $(call valgrind_tool_rule,$(tool)))) + +A''M_DISTCHECK_CONFIGURE_FLAGS ?= +A''M_DISTCHECK_CONFIGURE_FLAGS += --disable-valgrind + +MOSTLYCLEANFILES ?= +MOSTLYCLEANFILES += $(valgrind_log_files) + +.PHONY: check-valgrind $(add-prefix check-valgrind-,$(valgrind_tools)) +'] + + AC_SUBST([VALGRIND_CHECK_RULES]) + m4_ifdef([_AM_SUBST_NOTMAKE], [_AM_SUBST_NOTMAKE([VALGRIND_CHECK_RULES])]) +]) diff --git a/msan_ignorelist.txt b/msan_ignorelist.txt new file mode 100644 index 000000000..e435d8beb --- /dev/null +++ b/msan_ignorelist.txt @@ -0,0 +1,3 @@ +fun:__interceptor_strlen +fun:__interceptor_fopen64 +fun:__interceptor_memcmp \ No newline at end of file diff --git a/scripts/check-codeql-workflow.sh b/scripts/check-codeql-workflow.sh new file mode 100755 index 000000000..c47d85d33 --- /dev/null +++ b/scripts/check-codeql-workflow.sh @@ -0,0 +1,169 @@ +#!/usr/bin/env bash +# +# check-codeql-workflow.sh — structural gate for TASK-090 ("Harden CodeQL +# workflow and Codecov upload"). +# +# The v2-branch-gap audit flagged that `.github/workflows/codeql-analysis.yml` +# used deprecated, unpinned `@v1`/`@v2` action references and carried dead +# commented-out Autobuild scaffolding — it was never hardened to match the +# SHA-pinning convention `verify-build.yml` uses for every third-party action. +# TASK-090 pins every action to a full 40-hex commit SHA, deletes the Autobuild +# scaffolding, and keeps an explicit `./configure` + `make` build so CodeQL's +# C/C++ extractor traces the real compile commands. +# +# This LOCAL structural gate asserts the workflow stays hardened, guarding +# against silent drift back to floating tags or an autobuild. It mirrors the +# self-testing scripts/check-*.sh gate idiom paired with a +# scripts/test_check_*.sh unit test (see check-valgrind-lane.sh, +# check-parallel-install-lane.sh). +# +# Assertions against `.github/workflows/codeql-analysis.yml`: +# (a) the file parses as valid YAML (via python3 + PyYAML; NOT actionlint or +# yamllint, which are not guaranteed present on the runner); +# (b) NO unpinned action refs remain: no `uses: …@vN` floating-tag pins; +# (c) EVERY non-comment `uses:` line is pinned to a full 40-hex commit SHA; +# (d) Autobuild is gone: no `codeql-action/autobuild` reference survives, +# including in comments (the dead scaffolding must be deleted outright); +# (e) an explicit build is present: the workflow contains both a `./configure` +# and a `make` invocation so CodeQL's extractor traces the real compile; +# (f) the two CodeQL sub-actions (codeql-action/init and codeql-action/analyze) +# are both present and pinned to the SAME commit SHA — they ship from one +# repo/release and a split pin is a rotation bug. +# (g) a permissions block with security-events: write must be declared; +# (h) the libmicrohttpd download must be followed by a sha256sum -c +# verification step. +# +# Exit codes: +# 0 workflow is hardened correctly +# 1 one or more assertions failed +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +cd "$REPO_ROOT" + +WF="${1:-.github/workflows/codeql-analysis.yml}" + +echo "check-codeql-workflow: inspecting $WF" + +if [ ! -f "$WF" ]; then + echo "check-codeql-workflow: FAIL — workflow file not found: $WF" >&2 + exit 1 +fi + +fail=0 + +# (a) valid YAML. PyYAML parses the file (NOT actionlint/yamllint — not +# guaranteed present on the runner). The import-guard mirrors the other +# check-*.sh gates so a missing PyYAML is a clear, actionable message. +if ! python3 - "$WF" <<'PY' +import sys +try: + import yaml +except ImportError: + print(" PyYAML not importable — install with 'pip install pyyaml'", file=sys.stderr) + sys.exit(10) + +path = sys.argv[1] +try: + with open(path, encoding='utf-8') as fh: + yaml.safe_load(fh) +except Exception as e: # noqa: BLE001 - surface any parse failure + print(f" (a) YAML parse error: {e}", file=sys.stderr) + sys.exit(2) +print(" (a) YAML parses") +PY +then + echo " (a) FAILED" >&2 + fail=1 +fi + +# (b) No unpinned floating-tag action refs (`uses: …@vN`). A trailing +# `# vX.Y.Z` rotation comment on a SHA-pinned line is fine — the ref itself +# is `@`, not `@vN` — so the pattern requires `@v` directly. +if grep -nE 'uses:.*@v[0-9]+' "$WF"; then + echo " (b) unpinned floating-tag action ref(s) found — pin to a 40-hex SHA" >&2 + fail=1 +else + echo " (b) no floating-tag (@vN) action refs remain" +fi + +# (c) Every non-comment `uses:` line is pinned to a full 40-hex commit SHA. +uses_lines="$(grep -nE '^[[:space:]]*uses:' "$WF" || true)" +if [ -z "$uses_lines" ]; then + echo " (c) no `uses:` action steps found in the workflow" >&2 + fail=1 +else + bad_pins="$(printf '%s\n' "$uses_lines" \ + | grep -vE '@[0-9a-f]{40}([[:space:]]|$)' || true)" + if [ -n "$bad_pins" ]; then + echo " (c) uses: line(s) not pinned to a 40-hex SHA:" >&2 + printf '%s\n' "$bad_pins" | sed 's/^/ /' >&2 + fail=1 + else + echo " (c) every uses: line pinned to a 40-hex commit SHA" + fi +fi + +# (d) Autobuild must be gone entirely — including commented scaffolding. +if grep -nF 'codeql-action/autobuild' "$WF"; then + echo " (d) codeql-action/autobuild reference still present — delete it" >&2 + fail=1 +else + echo " (d) no codeql-action/autobuild reference remains" +fi + +# (sufficient: both must appear anywhere in the file, independent of step; +# false-positives from comments are negligible for this file's shape) +# (e) An explicit build must be present so CodeQL's C/C++ extractor traces +# the real compile commands (autobuild is deliberately not used). +if grep -qF './configure' "$WF" && grep -qE '(^|[[:space:]])make([[:space:]]|;|$)' "$WF"; then + echo " (e) explicit ./configure + make build step present" +else + echo " (e) explicit ./configure + make build step absent" >&2 + fail=1 +fi + +# (f) init and analyze must be pinned to the SAME commit SHA. +init_sha="$(grep -oE 'codeql-action/init@[0-9a-f]{40}' "$WF" | head -1 | cut -d@ -f2)" +analyze_sha="$(grep -oE 'codeql-action/analyze@[0-9a-f]{40}' "$WF" | head -1 | cut -d@ -f2)" +if [ -z "$init_sha" ]; then + echo " (f) codeql-action/init not present or not SHA-pinned" >&2 + fail=1 +elif [ -z "$analyze_sha" ]; then + echo " (f) codeql-action/analyze not present or not SHA-pinned" >&2 + fail=1 +elif [ "$init_sha" != "$analyze_sha" ]; then + echo " (f) codeql-action/init ($init_sha) and analyze ($analyze_sha) pinned to DIFFERENT SHAs" >&2 + fail=1 +else + echo " (f) codeql-action/init and analyze pinned to the same SHA ($init_sha)" +fi + +# (g) A permissions block with security-events: write must be declared. +# Without it the GITHUB_TOKEN defaults to the repo's global permission setting +# (potentially write-all on public repos). The CodeQL analyze step REQUIRES +# security-events: write to upload SARIF results to the security dashboard. +if grep -qE 'security-events:[[:space:]]*write' "$WF"; then + echo " (g) permissions block with security-events: write present" +else + echo " (g) missing permissions block with security-events: write — add 'permissions: security-events: write' to restrict the GITHUB_TOKEN" >&2 + fail=1 +fi + +# (h) The libmicrohttpd download must be followed by a sha256sum verification +# step. Without it a tampered S3 object would be compiled and executed inside +# the CodeQL runner with no detection. The checksum used by verify-build.yml +# (libmicrohttpd-1.0.3) is the authoritative source; both workflows must agree. +if grep -qF 'sha256sum -c' "$WF"; then + echo " (h) libmicrohttpd download checksum verification (sha256sum -c) present" +else + echo " (h) missing sha256sum verification for the libmicrohttpd download — add 'echo \"7816b57aae199cf5c3645e8770e1be5f0a4dfafbcb24b3772173dc4ee634126a libmicrohttpd-1.0.3.tar.gz\" | sha256sum -c' after the curl line" >&2 + fail=1 +fi + +if [ "$fail" -ne 0 ]; then + echo "check-codeql-workflow: FAIL — the CodeQL workflow is not fully hardened" >&2 + exit 1 +fi + +echo "check-codeql-workflow: PASS — CodeQL workflow SHA-pinned, autobuild-free, explicit build, least-privilege permissions, checksum-verified download" diff --git a/scripts/check-complexity.sh b/scripts/check-complexity.sh new file mode 100755 index 000000000..b797b6cef --- /dev/null +++ b/scripts/check-complexity.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# +# check-complexity.sh — enforce per-function cyclomatic complexity gate. +# +# Wraps `lizard` (https://pypi.org/project/lizard/) and fails the build +# if any function under src/ exceeds the configured CCN ceiling. +# +# Scope: src/ (library code only). test/, examples/, and build/ are not +# scanned — test fixtures legitimately stress branching depth and are +# not the subject of this gate. +# +# Threshold knobs: +# CCN_MAX max cyclomatic complexity per function (default below) +# LIZARD_EXTRA extra flags passed through to lizard (e.g. --length=200) +# +# CCN_MAX is set to 10, matching the project's wider mccabe convention. +# The bar was reached on feature/v2.0 via an incremental refactor that +# retired the 14 v1 offenders one commit at a time, with CCN_MAX +# ratcheting down each step so each commit was both a refactor and a +# tighter gate. New offenders must be brought below 10 at the same +# commit they are introduced; lifting CCN_MAX is not allowed. +# +# Exit codes: +# 0 no violations +# 1 one or more functions exceed CCN_MAX +# 2 lizard not installed +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +CCN_MAX="${CCN_MAX:-10}" + +# Prefer the standalone `lizard` entrypoint if it's on PATH; fall back to +# `python3 -m lizard` which is what `pip install --user lizard` produces +# when the user's pip bin dir is not on PATH (common on macOS). +if command -v lizard >/dev/null 2>&1; then + LIZARD=(lizard) +elif python3 -m lizard --version >/dev/null 2>&1; then + LIZARD=(python3 -m lizard) +else + echo "check-complexity: FAIL — lizard not installed" >&2 + echo " install with: pip3 install lizard" >&2 + exit 2 +fi + +cd "$REPO_ROOT" + +echo "check-complexity: scanning src/ with CCN_MAX=$CCN_MAX" +if ! "${LIZARD[@]}" -C "$CCN_MAX" --warnings_only ${LIZARD_EXTRA:-} src/; then + echo "check-complexity: FAIL — one or more functions exceed CCN $CCN_MAX" >&2 + echo " threshold lives in scripts/check-complexity.sh (CCN_MAX default)" >&2 + exit 1 +fi + +echo "check-complexity: PASS — no function exceeds CCN $CCN_MAX" diff --git a/scripts/check-deprecated-cookie-overload.sh b/scripts/check-deprecated-cookie-overload.sh new file mode 100755 index 000000000..7321f4b5d --- /dev/null +++ b/scripts/check-deprecated-cookie-overload.sh @@ -0,0 +1,98 @@ +#!/usr/bin/env bash +# +# check-deprecated-cookie-overload.sh — negative-compile gate pinning the +# [[deprecated]] attribute on the legacy string-blob cookie overload +# http_response::with_cookie(std::string, std::string) (TASK-064). +# +# test/unit/cookie_deprecation_sentinel_test.cpp pins the POSITIVE side +# (the deprecated overload still compiles behind a #pragma suppression +# block). The negative side — that the [[deprecated]] warning IS emitted +# on a suppression-free compile — was previously unverified: if someone +# removed the attribute, nothing would catch it (code-review finding on +# TASK-064). This gate closes that hole: +# +# 1. Positive control: a TU calling the structured overload +# with_cookie(cookie) MUST compile cleanly under +# -Werror=deprecated-declarations. This proves a failure in step 2 +# comes from the deprecation, not from a broken header or +# environment. +# 2. Negative check: a TU calling the deprecated overload +# with_cookie(std::string, std::string) MUST FAIL to compile under +# -Werror=deprecated-declarations, and the compiler output must +# mention "deprecated" (so an unrelated compile error cannot fake a +# pass). +# +# Both TUs include only the umbrella (public headers +# reject direct inclusion) and are compiled with -fsyntax-only, so no +# build tree, config.h, or libmicrohttpd is needed — the script runs +# standalone from a fresh checkout. -std=c++20 matches the umbrella's +# own version gate (httpserver.hpp #errors below C++20) and the +# CHECK_HEADERS_CXX invocation in Makefile.am. +# +# The compiler is ${CXX:-c++}; the Makefile target passes the configured +# $(CXX) through. +# +# Exit codes: +# 0 attribute in place (positive control compiles, negative TU fails +# with a deprecation diagnostic) +# 1 regression (deprecated overload compiled cleanly, positive +# control broke, or the negative TU failed for the wrong reason) +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +# The configured $(CXX) may be multi-word (e.g. "g++ -std=c++20"), so +# split it into an argv array rather than quoting it as one command name. +read -r -a CXX_CMD <<< "${CXX:-c++}" +COMMON_FLAGS=(-std=c++20 -fsyntax-only -I"$REPO_ROOT/src" + -Werror=deprecated-declarations) + +WORKDIR="$(mktemp -d "${TMPDIR:-/tmp}/check-deprecated-cookie.XXXXXX")" +trap 'rm -rf "$WORKDIR"' EXIT + +# Positive control: structured overload, must compile cleanly. +cat > "$WORKDIR/positive.cpp" <<'EOF' +#include +void structured_overload_ok(httpserver::http_response& r) { + r.with_cookie(httpserver::cookie{}.with_name("sid").with_value("v")); +} +EOF + +# Negative TU: deprecated legacy overload, must trip +# -Werror=deprecated-declarations. +cat > "$WORKDIR/negative.cpp" <<'EOF' +#include +#include +void deprecated_overload_trips(httpserver::http_response& r) { + r.with_cookie(std::string("sid"), std::string("v")); +} +EOF + +echo "check-deprecated-cookie-overload: compiler: ${CXX_CMD[*]}" + +if ! "${CXX_CMD[@]}" "${COMMON_FLAGS[@]}" "$WORKDIR/positive.cpp" \ + 2>"$WORKDIR/positive.log"; then + echo "check-deprecated-cookie-overload: FAIL — positive control did not compile" >&2 + echo " the structured with_cookie(cookie) TU must build cleanly under" >&2 + echo " -Werror=deprecated-declarations; fix the header (or this gate) first:" >&2 + sed 's/^/ /' "$WORKDIR/positive.log" >&2 + exit 1 +fi +echo " positive control: structured with_cookie(cookie) compiles cleanly" + +if "${CXX_CMD[@]}" "${COMMON_FLAGS[@]}" "$WORKDIR/negative.cpp" \ + 2>"$WORKDIR/negative.log"; then + echo "check-deprecated-cookie-overload: FAIL — with_cookie(std::string, std::string) compiled without a deprecation error" >&2 + echo " the [[deprecated]] attribute on the legacy overload in" >&2 + echo " src/httpserver/http_response.hpp has been removed or no longer fires" >&2 + exit 1 +fi + +if ! grep -qi "deprecated" "$WORKDIR/negative.log"; then + echo "check-deprecated-cookie-overload: FAIL — negative TU failed for the wrong reason" >&2 + echo " expected a -Wdeprecated-declarations diagnostic; got:" >&2 + sed 's/^/ /' "$WORKDIR/negative.log" >&2 + exit 1 +fi +echo " negative check: legacy with_cookie(string, string) trips -Werror=deprecated-declarations" + +echo "check-deprecated-cookie-overload: PASS — [[deprecated]] attribute is in place" diff --git a/scripts/check-doxygen.sh b/scripts/check-doxygen.sh new file mode 100755 index 000000000..74bf23d57 --- /dev/null +++ b/scripts/check-doxygen.sh @@ -0,0 +1,124 @@ +#!/usr/bin/env bash +# +# check-doxygen.sh — enforce TASK-043 invariant on the doxygen build. +# +# Runs `make doxygen-run` in the active build tree and asserts that the +# output contains zero substantive warnings or errors. Substantive means: +# anything from the doxygen parser about the SOURCE under +# `src/httpserver/` — e.g. an undocumented @param, a mismatched parameter +# name, a stale @copydoc target. +# +# Lines we deliberately filter OUT (environmental, not doc-content issues): +# +# E1. "Tag '' at line N of file '...doxyconfig.in' has become +# obsolete." — older config tags carried over from doxywizard. +# E2. "doxygen no longer ships with the FreeSans font." — packaging. +# E3. "the dot tool could not be found ..." — graphviz absent locally. +# E4. "Failed to rename ... .dot.png to ... .png" — dot post-processing +# failure (typically dot absent or installed late). +# E5. "Problems running dot: exit code=..." — same root cause as E4. +# E6. "Caller graph for '' not generated, too many nodes (N), +# threshold is M." — informational, DOT_GRAPH_MAX_NODES threshold. +# +# Everything else under the keywords `warning:` or `error:` is a real +# doc issue and FAILS the gate. Exits non-zero on the first violation. +# +# Behaviour when doxygen is not installed: SKIP (exit 0). The gate is +# CI-runnable on developer machines without doxygen; CI is expected to +# install it. This mirrors how scripts/check-readme.sh and friends are +# tolerant of missing tools. + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" + +# Locate the build tree. Prefer $BUILD_DIR, else REPO_ROOT/build, else +# fall back to the current working directory (assumes invoker is already +# inside the build dir, mirroring `make` behaviour). +BUILD_DIR="${BUILD_DIR:-}" +if [ -z "$BUILD_DIR" ]; then + if [ -d "$REPO_ROOT/build" ] && [ -f "$REPO_ROOT/build/Makefile" ]; then + BUILD_DIR="$REPO_ROOT/build" + elif [ -f "Makefile" ]; then + BUILD_DIR="$(pwd)" + else + echo "check-doxygen: FAIL: no build directory found (set BUILD_DIR)" >&2 + exit 1 + fi +fi + +# Sanity-guard: BUILD_DIR must be an absolute path to prevent a +# caller-controlled relative value from silently resolving to an +# unexpected location (e.g. via a symlink or a shell cd side-effect). +# This also catches the common mistake of exporting BUILD_DIR=build +# (relative) rather than BUILD_DIR="$(pwd)/build" (absolute). +if [[ "$BUILD_DIR" != /* ]]; then + echo "check-doxygen: FAIL: BUILD_DIR must be an absolute path (got: '$BUILD_DIR')" >&2 + exit 1 +fi + +if ! command -v doxygen >/dev/null 2>&1; then + echo "check-doxygen: SKIP — doxygen not installed (gate is enforced in CI)" + exit 0 +fi + +# mktemp semantics differ between macOS (-t places file under /var/folders) +# and Linux (-t places the template suffix after any system-chosen prefix). +# Using an explicit path under $BUILD_DIR is more portable and avoids +# leaving build metadata in world-readable /tmp on shared build hosts. +LOGFILE="$(mktemp "$BUILD_DIR/check-doxygen.XXXXXX")" +trap 'rm -f "$LOGFILE"' EXIT + +echo "check-doxygen: invoking 'make doxygen-run' in $BUILD_DIR" +# Force a fresh doxygen invocation. +# The make rule only fires when an input file is newer than the tag file. +# Removing the tag file forces the rule to fire every time so CI reruns +# and local re-checks always exercise the current source. +# NOTE: the tag file path is derived from Doxyfile OUTPUT_DIRECTORY and +# PROJECT_NAME; if either changes this rm becomes a no-op and the +# freshness guarantee is silently lost. Keep this path in sync with +# doxyconfig.in (currently: OUTPUT_DIRECTORY=doxygen-doc, PROJECT_NAME=libhttpserver). +rm -f "$BUILD_DIR/doxygen-doc/libhttpserver.tag" +if ! ( cd "$BUILD_DIR" && make doxygen-run ) >"$LOGFILE" 2>&1; then + echo "check-doxygen: FAIL: 'make doxygen-run' exited non-zero" >&2 + sed -n '1,200p' "$LOGFILE" >&2 + exit 1 +fi + +# Sanity-check that doxygen actually ran and produced output. +# A zero-byte log means doxygen was skipped by make (e.g. an incomplete +# freshness-force above) — treat this as a failure rather than a false PASS. +if [ ! -s "$LOGFILE" ]; then + echo "check-doxygen: FAIL: doxygen log is empty — doxygen may have been skipped by make" >&2 + exit 1 +fi + +# Strip the noisy-but-harmless environmental lines. The grep is portable +# (BSD/GNU): -E for ERE alternation, single anchored pattern per line. +# Representative match for each arm (from real doxygen output): +# E1: "Tag 'HTML_FOOTER' at line 42 of file 'doxyconfig.in' has become obsolete." +# E2: "doxygen no longer ships with the FreeSans font" +# E3: "the dot tool could not be found at /usr/bin/dot" +# E4: "Failed to rename /build/doxygen-doc/foo.dot.png to /build/doxygen-doc/foo.png" +# E5: "Problems running dot: exit code=1, command='dot ...'" +# E6: "Caller graph for 'webserver::start' not generated, too many nodes (300), threshold is 50." +FILTER='Tag .* has become obsolete\.' # E1: obsolete config tag +FILTER+='|doxygen no longer ships with the FreeSans font' # E2: font packaging +FILTER+='|the dot tool could not be found' # E3: graphviz absent +FILTER+='|Failed to rename .*\.dot\.png to' # E4: dot post-processing failure +FILTER+='|Problems running dot: exit code=' # E5: same root cause as E4 +FILTER+='|Caller graph for .* not generated, too many nodes' # E6: DOT_GRAPH_MAX_NODES + +REAL_WARNINGS="$(grep -E '(^|: )(warning|error):' "$LOGFILE" | grep -Ev "$FILTER" || true)" + +if [ -n "$REAL_WARNINGS" ]; then + echo "check-doxygen: FAIL: substantive doxygen warnings/errors found:" >&2 + echo "$REAL_WARNINGS" >&2 + echo "" >&2 + echo "(Full log: $LOGFILE — copy aside if needed before this script exits.)" >&2 + # Preserve log on failure for inspection. + trap - EXIT + exit 1 +fi + +echo "check-doxygen: PASS — doxygen-run produced zero substantive warnings" diff --git a/scripts/check-dr008-lanes.sh b/scripts/check-dr008-lanes.sh new file mode 100755 index 000000000..1fc9b7b12 --- /dev/null +++ b/scripts/check-dr008-lanes.sh @@ -0,0 +1,138 @@ +#!/usr/bin/env bash +# +# check-dr008-lanes.sh — structural gate for TASK-092 ("Wire the DR-008 +# concurrency/deadlock stress gates into per-PR CI"). +# +# TASK-092 added two dedicated CI steps to `.github/workflows/verify-build.yml` +# on top of the ordinary `make check` run: +# - "Run route_table_concurrency under TSan (TASK-092)": loops the DR-008 +# lock-order stress binary RTC_ITERATIONS times on the tsan lane via +# `make -C test check-route-table-concurrency`. +# - "Run stop()-from-handler deadlock contract (TASK-092)": exercises the +# DR-008 stop()-from-handler-deadlocks contract via +# `make -C test check-stop-from-handler`, on exactly one lane (the +# baseline Ubuntu gcc/nodebug/dynamic/classic job). +# +# Unlike the msan/valgrind/parallel-install gates, there is no real-time-cost +# reason these two steps could not run locally -- but a silent removal or +# misspelling of either step (or of their `if:` guard / `timeout-minutes:` +# safety net) would quietly drop DR-008 coverage from CI without failing any +# build. This LOCAL structural gate guards against that drift. Mirrors the +# self-testing scripts/check-*.sh gate idiom paired with a +# scripts/test_check_*.sh unit test (see check-msan-lane.sh, check-valgrind-lane.sh). +# +# Assertions against `.github/workflows/verify-build.yml`: +# (a) the file parses as valid YAML (via python3 + PyYAML; NOT actionlint +# or yamllint, which are not guaranteed present on the runner); +# (b) a step exists whose `run:` invokes +# `make -C test check-route-table-concurrency`, gated by an `if:` +# condition that references `matrix.build-type == 'tsan'`, and that +# carries a `timeout-minutes:` value (the documented failure mode -- +# a lock-order regression under TSan -- can hang); +# (c) a step exists whose `run:` invokes +# `make -C test check-stop-from-handler`, gated by an `if:` condition +# that pins it to the single baseline lane (`matrix.os-type == 'ubuntu'` +# and `matrix.build-type == 'classic'`), and that carries a +# `timeout-minutes:` value (the documented failure mode is a hang). +# +# Usage: check-dr008-lanes.sh [workflow-file] +# +# Exit codes: +# 0 workflow is wired correctly +# 1 one or more assertions failed +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +cd "$REPO_ROOT" + +WF="${1:-.github/workflows/verify-build.yml}" + +echo "check-dr008-lanes: inspecting $WF" + +if [ ! -f "$WF" ]; then + echo "check-dr008-lanes: FAIL — workflow file not found: $WF" >&2 + exit 1 +fi + +fail=0 + +if ! python3 - "$WF" <<'PY' +import sys + +try: + import yaml +except ImportError: + print(" PyYAML not importable — install with 'pip install pyyaml'", file=sys.stderr) + sys.exit(10) + +path = sys.argv[1] +try: + doc = yaml.safe_load(open(path)) +except Exception as e: # noqa: BLE001 - surface any parse failure + print(f" (a) YAML parse error: {e}", file=sys.stderr) + sys.exit(2) + +steps = None +for job_name, job in (doc.get("jobs") or {}).items(): + if not isinstance(job, dict): + continue + candidate = job.get("steps") + if isinstance(candidate, list): + steps = candidate + break +if steps is None: + print(" could not locate a job with a `steps:` list", file=sys.stderr) + sys.exit(3) + +problems = [] + +def find_step(invocation): + matches = [s for s in steps + if isinstance(s, dict) and invocation in (s.get("run") or "")] + if not matches: + problems.append(f"no step found invoking `{invocation}`") + return None + if len(matches) > 1: + problems.append(f"expected exactly one step invoking `{invocation}`, found {len(matches)}") + return matches[0] + +rtc = find_step("make -C test check-route-table-concurrency") +if rtc is not None: + if_cond = rtc.get("if") or "" + if "matrix.build-type == 'tsan'" not in if_cond: + problems.append( + "check-route-table-concurrency step's `if:` does not gate on " + f"matrix.build-type == 'tsan' (got: {if_cond!r})") + if rtc.get("timeout-minutes") is None: + problems.append("check-route-table-concurrency step has no timeout-minutes") + +sfh = find_step("make -C test check-stop-from-handler") +if sfh is not None: + if_cond = sfh.get("if") or "" + if "matrix.os-type == 'ubuntu'" not in if_cond or "matrix.build-type == 'classic'" not in if_cond: + problems.append( + "check-stop-from-handler step's `if:` is not pinned to the " + f"baseline ubuntu/classic lane (got: {if_cond!r})") + if sfh.get("timeout-minutes") is None: + problems.append("check-stop-from-handler step has no timeout-minutes") + +if problems: + for p in problems: + print(f" (b)/(c) {p}", file=sys.stderr) + sys.exit(4) + +print(" (a) YAML parses; (b) check-route-table-concurrency step present, " + "tsan-gated, timeout-boxed; (c) check-stop-from-handler step present, " + "baseline-lane-gated, timeout-boxed") +PY +then + echo " (a)/(b)/(c) FAILED" >&2 + fail=1 +fi + +if [ "$fail" -ne 0 ]; then + echo "check-dr008-lanes: FAIL — the DR-008 stress gates are not fully wired" >&2 + exit 1 +fi + +echo "check-dr008-lanes: PASS — DR-008 concurrency/deadlock stress gates structurally wired" diff --git a/scripts/check-duplication.sh b/scripts/check-duplication.sh new file mode 100755 index 000000000..8092d048a --- /dev/null +++ b/scripts/check-duplication.sh @@ -0,0 +1,66 @@ +#!/usr/bin/env bash +# +# check-duplication.sh — enforce zero copy/paste duplication above a +# token-count threshold. +# +# Wraps PMD's CPD (Copy/Paste Detector) with the C++ tokenizer. CPD +# understands C++ tokens (templates, preprocessor, etc.) so it avoids +# the false positives a generic text-token tool would produce on a +# header-heavy library like this one. +# +# Scope: src/ (library code only). test/ and examples/ legitimately +# duplicate fixture and demo scaffolding and are not the subject of +# this gate. +# +# Threshold knob: +# CPD_MIN_TOKENS minimum token-run length flagged as a duplicate +# (default 100 — PMD's own default, a sensible C++ +# starting point). +# +# Exit codes: +# 0 no duplicates above CPD_MIN_TOKENS +# 1 duplicate(s) found (PMD CPD exits 4 → remapped here to 1) +# 2 pmd not installed +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +CPD_MIN_TOKENS="${CPD_MIN_TOKENS:-100}" + +if ! command -v pmd >/dev/null 2>&1; then + echo "check-duplication: FAIL — pmd not installed" >&2 + echo " install with: brew install pmd (or download from https://pmd.github.io/)" >&2 + exit 2 +fi + +cd "$REPO_ROOT" + +echo "check-duplication: scanning src/ with CPD_MIN_TOKENS=$CPD_MIN_TOKENS" + +# PMD CPD exit codes: +# 0 clean +# 4 duplicate(s) found (--fail-on-violation is true by default) +# 5 recoverable errors (e.g. a file failed to tokenize) +# We remap 4 -> 1 to fit the conventional "1 == policy violation" shell +# convention, and let 5 propagate as-is so a tokenizer crash isn't +# silently treated as a pass. +status=0 +pmd cpd \ + --language cpp \ + --minimum-tokens "$CPD_MIN_TOKENS" \ + --dir src \ + --format text || status=$? + +case "$status" in + 0) + echo "check-duplication: PASS — no duplicates at or above $CPD_MIN_TOKENS tokens" + ;; + 4) + echo "check-duplication: FAIL — duplicate code at or above $CPD_MIN_TOKENS tokens" >&2 + echo " threshold lives in scripts/check-duplication.sh (CPD_MIN_TOKENS default)" >&2 + exit 1 + ;; + *) + echo "check-duplication: FAIL — pmd cpd exited with status $status" >&2 + exit "$status" + ;; +esac diff --git a/scripts/check-examples.sh b/scripts/check-examples.sh new file mode 100755 index 000000000..828e15943 --- /dev/null +++ b/scripts/check-examples.sh @@ -0,0 +1,215 @@ +#!/usr/bin/env bash +# +# check-examples.sh — enforce TASK-040 invariants on examples/. +# +# This script is a static-source check, not a build check. It asserts that: +# +# 1. examples/hello_world.cpp exists, is at most 10 lines (by the rule +# defined below), defines no class deriving from http_resource, and +# registers nothing via raw pointers. +# +# 2. examples/shared_state.cpp exists, defines a class deriving from +# http_resource, holds a std::mutex, overrides both render_get and +# render_post, and registers itself via register_path with +# std::make_unique. +# +# 3. Every program listed in examples/Makefile.am noinst_PROGRAMS has a +# corresponding .cpp source file on disk. This catches renames or +# deletions that would silently break `make examples`. +# +# LOC counting rule (TASK-040 plan D2): +# The LOC count is the number of non-empty, non-comment lines from the +# first non-comment line to EOF, after stripping any leading /* ... */ +# license header. Implemented with awk below. +# +# Exits non-zero on the first violation. + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +HELLO="$REPO_ROOT/examples/hello_world.cpp" +SHARED="$REPO_ROOT/examples/shared_state.cpp" +MAKEFILE_AM="$REPO_ROOT/examples/Makefile.am" + +fail() { + echo "check-examples: FAIL: $*" >&2 + exit 1 +} + +count_loc() { + # Strip leading /* ... */ block(s), then count non-blank non-// lines. + awk ' + BEGIN { in_block = 0; seen_first = 0 } + { + line = $0 + # Handle block comments. + if (in_block) { + if (line ~ /\*\//) { in_block = 0; sub(/.*\*\//, "", line) } + else { next } + } + # Strip inline block-open comments that close on the same line. + while (match(line, /\/\*[^*]*\*\//)) { + line = substr(line, 1, RSTART-1) substr(line, RSTART+RLENGTH) + } + if (match(line, /\/\*/)) { + in_block = 1 + line = substr(line, 1, RSTART-1) + } + # Strip line comments. + sub(/\/\/.*/, "", line) + # Trim whitespace. + gsub(/^[ \t\r]+|[ \t\r]+$/, "", line) + if (line == "") next + count++ + } + END { print count + 0 } + ' "$1" +} + +# ---- hello_world.cpp ---------------------------------------------------- + +[ -f "$HELLO" ] || fail "examples/hello_world.cpp does not exist" + +loc=$(count_loc "$HELLO") +if [ "$loc" -gt 10 ]; then + fail "examples/hello_world.cpp is $loc LOC, must be <= 10" +fi +if [ "$loc" -lt 5 ]; then + fail "examples/hello_world.cpp is $loc LOC, suspiciously short (must be >= 5)" +fi + +if grep -Eq 'class[[:space:]]+[A-Za-z_][A-Za-z_0-9]*[[:space:]]*:[[:space:]]*public[[:space:]]+[A-Za-z_:]*http_resource' "$HELLO"; then + fail "examples/hello_world.cpp defines an http_resource subclass; the lambda form is required" +fi + +# No raw-pointer registration like ws.register_path("/x", new Foo()). +if grep -Eq 'register_(path|prefix|resource)[[:space:]]*\([^,]*,[[:space:]]*new[[:space:]]+' "$HELLO"; then + fail "examples/hello_world.cpp uses raw-pointer registration; use lambda or smart-pointer form" +fi + +# ---- shared_state.cpp --------------------------------------------------- + +[ -f "$SHARED" ] || fail "examples/shared_state.cpp does not exist" + +grep -Eq ':[[:space:]]*public[[:space:]]+[A-Za-z_:]*http_resource' "$SHARED" \ + || fail "examples/shared_state.cpp must define a class inheriting from http_resource" + +grep -q 'std::mutex' "$SHARED" \ + || fail "examples/shared_state.cpp must use std::mutex to demonstrate locking" + +grep -Eq '\brender_get\b' "$SHARED" \ + || fail "examples/shared_state.cpp must override render_get" + +grep -Eq '\brender_post\b' "$SHARED" \ + || fail "examples/shared_state.cpp must override render_post" + +grep -Eq 'register_path[[:space:]]*\(.*std::make_unique' "$SHARED" \ + || fail "examples/shared_state.cpp must register via register_path with std::make_unique" + +# ---- Makefile.am coverage --------------------------------------------------- +# Two-directional check: +# (a) Every program listed in noinst_PROGRAMS must have a .cpp source file. +# (b) Every .cpp in examples/ must be listed in noinst_PROGRAMS or be an +# explicitly acknowledged non-program artifact. +# +# KNOWN_ARTIFACTS: .cpp files that are intentionally not in noinst_PROGRAMS. +# Empty by design: every example .cpp — including client_cert_auth.cpp, now +# built under the HAVE_GNUTLS conditional in examples/Makefile.am (TASK-091) — +# is listed in noinst_PROGRAMS, so nothing is allow-listed out of coverage. +KNOWN_ARTIFACTS="" + +[ -f "$MAKEFILE_AM" ] || fail "examples/Makefile.am does not exist" + +# Extract all tokens from noinst_PROGRAMS lines (handles = and +=). +# Strip the variable name and assignment operator, then collect program names. +# Use awk for portability (avoids sed \s and \? which differ between GNU/BSD). +# Collect into a real bash array (rather than a flat string) so the loops +# below iterate over exact program names instead of word-splitting an +# unquoted string. Uses a while-read loop rather than `mapfile` for bash +# 3.x / macOS compatibility (same pattern as check-warning-suppressions.sh +# and check-server-ready-helper.sh); the `tr` step explodes any multi-name +# awk output line into one program name per line/array element, preserving +# the original word-splitting semantics without the unquoted-expansion risk. +programs=() +while IFS= read -r prog; do + [ -n "$prog" ] && programs+=("$prog") +done < <(awk '/^[[:space:]]*noinst_PROGRAMS[[:space:]]*(=|\+=)/ { + sub(/^[[:space:]]*noinst_PROGRAMS[[:space:]]*(=|\+=)[[:space:]]*/, "") + gsub(/\\$/, "") + print +}' "$MAKEFILE_AM" | tr -s '[:space:]' '\n') + +# (a) Makefile.am → disk: every listed program must have a .cpp file. +missing=0 +for prog in ${programs[@]+"${programs[@]}"}; do + src="$REPO_ROOT/examples/${prog}.cpp" + if [ ! -f "$src" ]; then + echo "check-examples: FAIL: noinst_PROGRAMS lists '$prog' but examples/${prog}.cpp does not exist" >&2 + missing=$((missing + 1)) + fi +done +if [ "$missing" -gt 0 ]; then + fail "$missing program(s) listed in Makefile.am noinst_PROGRAMS have no .cpp source" +fi + +# (b) disk → Makefile.am: every .cpp must be in noinst_PROGRAMS or KNOWN_ARTIFACTS. +unlisted=0 +for src in "$REPO_ROOT"/examples/*.cpp; do + base="$(basename "$src" .cpp)" + # Check if in noinst_PROGRAMS. + found=0 + for prog in ${programs[@]+"${programs[@]}"}; do + if [ "$prog" = "$base" ]; then + found=1 + break + fi + done + if [ "$found" -eq 0 ]; then + # Check if in KNOWN_ARTIFACTS allowlist. + in_artifacts=0 + for artifact in $KNOWN_ARTIFACTS; do + if [ "$artifact" = "$base" ]; then + in_artifacts=1 + break + fi + done + if [ "$in_artifacts" -eq 0 ]; then + echo "check-examples: FAIL: examples/${base}.cpp is on disk but not listed in noinst_PROGRAMS or KNOWN_ARTIFACTS" >&2 + unlisted=$((unlisted + 1)) + fi + fi +done +if [ "$unlisted" -gt 0 ]; then + fail "$unlisted .cpp file(s) in examples/ are not listed in Makefile.am noinst_PROGRAMS — add them or add to KNOWN_ARTIFACTS" +fi + +# ---- TASK-052: hook-example documentation coverage -------------------------- +# The four lifecycle-hook examples (denied_ip_log, early_413, clf_access_log, +# per_route_auth) must be listed in both examples/README.md and the top-level +# README.md so the user-visible resolution of issues #332, #281, #69, #273 is +# discoverable. Per TASK-052 / Phase 3. +EXAMPLES_README="$REPO_ROOT/examples/README.md" +TOP_README="$REPO_ROOT/README.md" +HOOK_EXAMPLES="denied_ip_log early_413 clf_access_log per_route_auth" + +for f in "$EXAMPLES_README" "$TOP_README"; do + [ -f "$f" ] || fail "$(basename "$f") does not exist" +done + +missing_doc=0 +for ex in $HOOK_EXAMPLES; do + if ! grep -q "${ex}\\.cpp" "$EXAMPLES_README"; then + echo "check-examples: FAIL: examples/README.md does not mention ${ex}.cpp" >&2 + missing_doc=$((missing_doc + 1)) + fi + if ! grep -q "${ex}\\.cpp" "$TOP_README"; then + echo "check-examples: FAIL: README.md does not mention ${ex}.cpp" >&2 + missing_doc=$((missing_doc + 1)) + fi +done +if [ "$missing_doc" -gt 0 ]; then + fail "$missing_doc hook-example reference(s) missing from README docs (TASK-052)" +fi + +echo "check-examples: OK (hello_world.cpp = $loc LOC; shared_state.cpp asserted; Makefile.am coverage verified bidirectionally; hook examples listed in both READMEs)" +exit 0 diff --git a/scripts/check-file-size.sh b/scripts/check-file-size.sh new file mode 100755 index 000000000..74551ed96 --- /dev/null +++ b/scripts/check-file-size.sh @@ -0,0 +1,117 @@ +#!/usr/bin/env bash +# +# check-file-size.sh — enforce a per-file line-count ceiling on the C++ +# library sources. +# +# Scope: src/ (library code only). test/ and examples/ are not scanned — +# test fixtures and demo scaffolding legitimately grow large and are not +# the subject of this gate. +# +# Scanned extensions: .cpp, .hpp (matches cpplint's --extensions=cpp,hpp +# and the v2.0 source convention; no .h / .cc in src/). +# +# Metric: source lines of code (SLOC) — physical lines with blank lines, +# comment-only lines, and single-character lines (a lone `{`, `}`, `(`, +# `)`, or `;`) excluded. A dependency-free awk pass strips C/C++ +# comments (`//` to end-of-line and `/* ... */` spans, including +# multi-line) and blank lines, then counts what remains. The stripper is +# string-literal aware so a `//` or `/*` inside a quoted string cannot make +# the counter drop real code. Comments are excluded deliberately: this +# codebase documents heavily, and a physical-line ceiling taxes exactly the +# documentation the project invests in. The per-function complexity gate +# already covers semantic density. +# +# Threshold knobs: +# FILE_LOC_MAX maximum SLOC (code lines) per file (default below) +# FACADE_LOC_MAX higher, still-bounded limit for the named façade/adapter +# carve-out (see FACADE_ALLOWLIST below) +# +# Long-term target: 500 lines, matching the per-module ceiling used by +# the sibling project under ../artistai (radon SLOC) and the natural +# break point where the remaining files already comply. +# +# Exit codes: +# 0 no violations +# 1 one or more files exceed FILE_LOC_MAX +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +# Enforced long-term ceiling: 500 lines (see the rationale above). Set +# via the environment to test against a tighter bar locally. +FILE_LOC_MAX="${FILE_LOC_MAX:-500}" + +# Carve-out (DR-014 "one class, many files" discussion): the webserver public +# façade and its MHD C-ABI trampoline adapter are a single class's surface that +# is inherently wide and cannot be split further without arbitrary API-area +# fragmentation. Rather than scatter one class across many small files just to +# satisfy the line ceiling, these named files are held to a higher -- but still +# bounded -- limit. This is a deliberate, visible exception (logged below and +# enforced against FACADE_LOC_MAX), NOT a blanket exemption: growth past that +# bound still fails the gate and forces a real look. +FACADE_LOC_MAX="${FACADE_LOC_MAX:-600}" +FACADE_ALLOWLIST=( + "src/webserver.cpp" # the entire webserver public API + "src/detail/webserver_callbacks.cpp" # the MHD C-ABI trampoline adapter +) + +cd "$REPO_ROOT" + +echo "check-file-size: scanning src/ with FILE_LOC_MAX=$FILE_LOC_MAX (SLOC; comments and blank lines excluded)" +echo "check-file-size: façade/adapter carve-out at FACADE_LOC_MAX=$FACADE_LOC_MAX -> ${FACADE_ALLOWLIST[*]}" + +# count_sloc FILE — echo the count of source lines (comment- and blank-only +# lines removed). A small state machine tracks block-comment and string +# state across characters so that `//` or `/*` inside a string literal is +# not mistaken for a comment (which would otherwise let the counter silently +# drop real code and mask an oversize file). +count_sloc() { + awk ' + { + line = $0; n = length(line); i = 1; out = "" + while (i <= n) { + c = substr(line, i, 1); two = substr(line, i, 2) + if (in_block) { if (two == "*/") { in_block = 0; i += 2 } else i++; continue } + if (in_str) { + out = out c + if (c == "\\") { out = out substr(line, i + 1, 1); i += 2; continue } + if (c == "\"") in_str = 0 + i++; continue + } + if (two == "/*") { in_block = 1; i += 2; continue } + if (two == "//") break # rest of line is a comment + if (c == "\"") { in_str = 1; out = out c; i++; continue } + out = out c; i++ + } + gsub(/[ \t\r]/, "", out) + # Exclude lines that reduce to a single character — a lone brace, + # paren, bracket, or semicolon (`{`, `}`, `(`, `)`, `;`). These are + # pure scaffolding: they carry no logic and their count is an + # artifact of brace style, not of how much a file actually does. + if (length(out) > 1) count++ + } + END { print count + 0 } + ' "$1" +} + +violations=0 +# -print0/read -d '' keeps the loop safe against any future filename with +# whitespace, even though src/ today is plain ASCII. +while IFS= read -r -d '' file; do + lines=$(count_sloc "$file") + max="$FILE_LOC_MAX" + for exempt in "${FACADE_ALLOWLIST[@]}"; do + if [ "$file" = "$exempt" ]; then max="$FACADE_LOC_MAX"; break; fi + done + if [ "$lines" -gt "$max" ]; then + echo " $file: $lines SLOC (max $max)" >&2 + violations=$((violations + 1)) + fi +done < <(find src -type f \( -name '*.cpp' -o -name '*.hpp' \) -print0) + +if [ "$violations" -gt 0 ]; then + echo "check-file-size: FAIL — $violations file(s) exceed FILE_LOC_MAX=$FILE_LOC_MAX" >&2 + echo " threshold lives in scripts/check-file-size.sh (FILE_LOC_MAX default)" >&2 + exit 1 +fi + +echo "check-file-size: PASS — all files within limits (FILE_LOC_MAX=$FILE_LOC_MAX; carve-out FACADE_LOC_MAX=$FACADE_LOC_MAX)" diff --git a/scripts/check-hooks-doc-spotcheck.sh b/scripts/check-hooks-doc-spotcheck.sh new file mode 100755 index 000000000..5c1fd92eb --- /dev/null +++ b/scripts/check-hooks-doc-spotcheck.sh @@ -0,0 +1,214 @@ +#!/usr/bin/env bash +# +# check-hooks-doc-spotcheck.sh — TASK-052 documentation regression gate. +# +# Asserts that the hook bus public surface carries the documentation the +# v2.0 PRD / DR-012 / §4.10 / §5.6 promised: +# +# H1. The four public hook headers (hook_phase, hook_action, hook_handle, +# hook_context) each carry a `/** @file ... */` block. +# H2. Each header's principal public symbol carries an `@brief` block. +# H3. webserver::add_hook (in webserver_hooks.hpp) enumerates the eleven +# phases AND has dropped the stale TASK-045 "skeleton-only" caveat. +# H4. http_resource::add_hook (in http_resource.hpp) enumerates the five +# permitted phases AND names the six rejected ones. +# H5. Each of the five v1 alias setters (log_access, not_found_handler, +# method_not_allowed_handler, internal_error_handler, auth_handler) +# carries the "this is an alias for add_hook" callout in its Doxygen +# block. +# H6. The webserver class-level Threading-contract block mentions hook +# concurrency. +# +# This is a static grep-based gate -- it does NOT shell out to doxygen. +# `make doxygen-run` is the heavier independent gate. + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +SRC="$REPO_ROOT/src/httpserver" + +# Header doc-comment invariants are platform-independent and fully enforced on +# the Linux and macOS lanes. This gate relies on GNU/POSIX grep behavior that is +# unreliable under the MSYS2/mingw shell (git autocrlf CRLF + word-boundary +# matching), so skip on Windows rather than re-validate identical repo content. +case "$(uname -s)" in + MINGW*|MSYS*|CYGWIN*) + echo "check-hooks-doc-spotcheck: SKIP on Windows/MSYS (content gate enforced on POSIX lanes)" + exit 0 + ;; +esac + +fail() { + echo "check-hooks-doc-spotcheck: FAIL: $*" >&2 + exit 1 +} + +# ---- H1: file-level @file blocks -------------------------------------------- + +for f in hook_phase.hpp hook_action.hpp hook_handle.hpp hook_context.hpp; do + full="$SRC/$f" + [ -f "$full" ] || fail "H1: $full does not exist" + if ! grep -qE '^[[:space:]]*\* @file '"$f"'\b' "$full"; then + fail "H1: $f is missing a '@file $f' Doxygen tag" + fi +done + +# ---- H2: principal public symbols have @brief blocks ------------------------ + +require_brief_before() { + # $1 = file, $2 = regex matching the symbol's declaration line + local file="$1" sym_re="$2" + # Find the line number of the symbol declaration, then scan upward + # for the nearest /** block and ensure it carries @brief. + local sym_line + sym_line="$(grep -nE "$sym_re" "$file" | head -1 | cut -d: -f1)" || true + if [ -z "$sym_line" ]; then + fail "H2: cannot find symbol matching '$sym_re' in $file" + fi + # Walk back up to 60 lines and find the most recent '/**'. + local start_line + start_line="$(awk -v end="$sym_line" 'NR=start && NR<=end && /\*\// { print NR; exit }' "$file")" + if [ -z "$end_line" ]; then + fail "H2: unterminated /** block preceding '$sym_re' in $(basename "$file")" + fi + if ! awk -v s="$start_line" -v e="$end_line" 'NR>=s && NR<=e' "$file" | grep -qE '@brief\b'; then + fail "H2: /** block preceding '$sym_re' in $(basename "$file") has no @brief" + fi +} + +require_brief_before "$SRC/hook_phase.hpp" '^enum class hook_phase ' +require_brief_before "$SRC/hook_phase.hpp" '^constexpr std::string_view to_string\(hook_phase' +require_brief_before "$SRC/hook_action.hpp" '^class hook_action ' +require_brief_before "$SRC/hook_handle.hpp" '^class hook_handle ' +require_brief_before "$SRC/hook_context.hpp" '^struct peer_address ' +require_brief_before "$SRC/hook_context.hpp" '^struct before_handler_ctx ' +require_brief_before "$SRC/hook_context.hpp" '^struct response_sent_ctx ' +require_brief_before "$SRC/hook_context.hpp" '^struct request_completed_ctx ' + +# ---- H3: webserver_hooks.hpp -- eleven phases, no stale skeleton caveat ---- + +WS_HOOKS="$SRC/webserver_hooks.hpp" +[ -f "$WS_HOOKS" ] || fail "H3: $WS_HOOKS does not exist" +if ! grep -qiE '(eleven|11)[[:space:]]+overloads' "$WS_HOOKS" && \ + ! grep -qiE '(eleven|11)[[:space:]]+phases' "$WS_HOOKS"; then + fail "H3: webserver_hooks.hpp Doxygen block must mention 'eleven' (or '11') phases / overloads" +fi +for phase in connection_opened accept_decision connection_closed request_received \ + body_chunk route_resolved before_handler handler_exception \ + after_handler response_sent request_completed; do + if ! grep -qE "\b${phase}\b" "$WS_HOOKS"; then + fail "H3: webserver_hooks.hpp Doxygen block does not name phase '$phase'" + fi +done +if grep -qiE 'skeleton-only at TASK-045' "$WS_HOOKS"; then + fail "H3: webserver_hooks.hpp still contains the stale 'Skeleton-only at TASK-045' note (the feature is fully wired post-TASK-051)" +fi + +# ---- H4: http_resource::add_hook lists five permitted + six rejected ------- + +RES_HPP="$SRC/http_resource.hpp" +[ -f "$RES_HPP" ] || fail "H4: $RES_HPP does not exist" +res_block="$(awk ' + BEGIN { in_block = 0; printed = 0; saved = "" } + /\/\*\*/ { in_block = 1; saved = $0; next } + in_block { saved = saved "\n" $0 } + in_block && /\*\// { in_block = 2; next } + in_block == 2 && /hook_handle add_hook\(hook_phase phase,/ { + print saved; printed = 1; exit + } + in_block == 2 { in_block = 0; saved = "" } +' "$RES_HPP")" +if [ -z "$res_block" ]; then + fail "H4: http_resource.hpp has no /** block immediately preceding add_hook(hook_phase, ...)" +fi +if ! echo "$res_block" | grep -qiE '(five|5)[[:space:]]+permitted[[:space:]]+phases|five[[:space:]]+post-route-resolution'; then + fail "H4: http_resource.hpp add_hook block must mention 'five' permitted phases" +fi +for phase in before_handler handler_exception after_handler response_sent request_completed; do + if ! echo "$res_block" | grep -qE "\b${phase}\b"; then + fail "H4: http_resource.hpp add_hook block does not name permitted phase '$phase'" + fi +done +for phase in connection_opened accept_decision connection_closed request_received body_chunk route_resolved; do + if ! echo "$res_block" | grep -qE "\b${phase}\b"; then + fail "H4: http_resource.hpp add_hook block does not name rejected phase '$phase'" + fi +done +if ! echo "$res_block" | grep -qE 'invalid_argument'; then + fail "H4: http_resource.hpp add_hook block must document the std::invalid_argument throw" +fi + +# ---- H5: alias-setter Doxygen mentions add_hook + hook_phase + alias ------- + +CW_HPP="$SRC/create_webserver.hpp" +# TASK-086: the handler/callback alias setters (not_found_handler, +# method_not_allowed_handler, internal_error_handler, auth_handler) were +# split out of create_webserver.hpp into the create_webserver_setters.hpp +# class-body fragment to keep both headers under the per-file LOC ceiling. +# log_access remains in create_webserver.hpp. Locate each setter in +# whichever of the two files declares it. +CW_SETTERS="$SRC/create_webserver_setters.hpp" +[ -f "$CW_HPP" ] || fail "H5: $CW_HPP does not exist" +[ -f "$CW_SETTERS" ] || fail "H5: $CW_SETTERS does not exist" +for setter in log_access not_found_handler method_not_allowed_handler \ + internal_error_handler auth_handler; do + cw_file="$CW_HPP" + sym_line="$(grep -nE "^[[:space:]]*create_webserver& ${setter}\(" "$cw_file" \ + | head -1 | cut -d: -f1)" || true + if [ -z "$sym_line" ]; then + cw_file="$CW_SETTERS" + sym_line="$(grep -nE "^[[:space:]]*create_webserver& ${setter}\(" "$cw_file" \ + | head -1 | cut -d: -f1)" || true + fi + if [ -z "$sym_line" ]; then + fail "H5: cannot find setter '$setter' in create_webserver.hpp or create_webserver_setters.hpp" + fi + start_line="$(awk -v end="$sym_line" 'NR=s && NR<=e && /\*\// { print NR; exit }' "$cw_file")" + block="$(awk -v s="$start_line" -v e="$end_line" 'NR>=s && NR<=e' "$cw_file")" + if ! echo "$block" | grep -qiE '\balias\b|\baliases\b'; then + fail "H5: setter '$setter' Doxygen block does not call out 'alias'" + fi + if ! echo "$block" | grep -qE '\badd_hook\b'; then + fail "H5: setter '$setter' Doxygen block does not mention add_hook" + fi + if ! echo "$block" | grep -qE '\bhook_phase::'; then + fail "H5: setter '$setter' Doxygen block does not mention a hook_phase:: value" + fi +done + +# ---- H6: webserver class-level threading block mentions hook concurrency --- + +WS_HPP="$SRC/webserver.hpp" +[ -f "$WS_HPP" ] || fail "H6: $WS_HPP does not exist" +# Extract from the '### Threading contract' marker to the next '###' / '*/'. +thread_block="$(awk ' + /\*[[:space:]]+###[[:space:]]+Threading contract/ { in_block = 1; next } + in_block && /\*[[:space:]]+###[[:space:]]+/ { exit } + in_block && /\*\// { exit } + in_block { print } +' "$WS_HPP")" +if [ -z "$thread_block" ]; then + fail "H6: webserver.hpp class-level Doxygen has no '### Threading contract' block" +fi +if ! echo "$thread_block" | grep -qE 'add_hook|hook[[:space:]]*\('; then + fail "H6: Threading-contract block does not mention hooks / add_hook" +fi +if ! echo "$thread_block" | grep -qiE 'may[[:space:]]+run[[:space:]]+concurrently'; then + fail "H6: Threading-contract block must include the 'may run concurrently' wording for hooks" +fi +if ! echo "$thread_block" | grep -qiE 'thread-safe'; then + fail "H6: Threading-contract block must mention thread-safety" +fi + +echo "check-hooks-doc-spotcheck: OK (H1 @file blocks; H2 @brief on principal symbols; H3 eleven phases in webserver_hooks.hpp; H4 five-permitted / six-rejected on http_resource::add_hook; H5 alias callouts on five v1 setters; H6 hook concurrency in webserver threading block)" +exit 0 diff --git a/scripts/check-msan-lane.sh b/scripts/check-msan-lane.sh new file mode 100755 index 000000000..7839296de --- /dev/null +++ b/scripts/check-msan-lane.sh @@ -0,0 +1,165 @@ +#!/usr/bin/env bash +# +# check-msan-lane.sh — structural gate for TASK-087 ("Restore msan CI lane"). +# +# The v2-branch-gap audit flagged that the MemorySanitizer (msan) CI lane +# went silently absent: its matrix entry in +# `.github/workflows/verify-build.yml` was commented out when Ubuntu 18.04 / +# clang-6.0 were retired, and asan/lsan/tsan/ubsan carried on without it. +# TASK-087 restores a current, instrumented-libc++ msan lane. +# +# A full GitHub Actions msan build cannot run on a developer's machine (it +# needs an instrumented libc++ and libmicrohttpd built in-CI), so acceptance +# is split: this LOCAL structural gate asserts the workflow is wired +# correctly, and the REAL green run happens on the PR. This mirrors the +# repo idiom of a self-testing `scripts/check-*.sh` gate paired with a +# `scripts/test_check_*.sh` unit test (see check-warning-suppressions.sh). +# +# Assertions against `.github/workflows/verify-build.yml`: +# (a) the file parses as valid YAML (via python3 + PyYAML; NOT actionlint +# or yamllint, which are not guaranteed present on the runner); +# (b) a matrix `include:` entry with `build-type: msan` exists and carries +# the keys mirrored from the asan entry (test-group/os/os-type/ +# compiler-family/c-compiler/cc-compiler/debug/coverage/shell); +# (c) the stale `ubuntu-18.04` / `clang-6.0` commented msan block is GONE; +# (d) the "Run tests" step wires `MSAN_OPTIONS` and a scoped `TESTS=` +# invocation (msan coverage is deliberately scoped to backend-free +# unit tests because curl is uninstrumented). +# +# Exit codes: +# 0 workflow is wired correctly +# 1 one or more assertions failed +# +# (Internally, the embedded python3 + PyYAML helper below uses its own +# distinguished exit codes -- notably 10 for "PyYAML not importable" -- to +# let the caller tell a missing local dependency apart from a genuine +# parse/structural failure. That distinction is used to print a SKIP +# notice instead of a hard FAIL for assertions (a)/(b); it does not change +# this script's own 0/1 contract above.) +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +cd "$REPO_ROOT" + +WF="${1:-.github/workflows/verify-build.yml}" + +echo "check-msan-lane: inspecting $WF" + +if [ ! -f "$WF" ]; then + echo "check-msan-lane: FAIL — workflow file not found: $WF" >&2 + exit 1 +fi + +fail=0 + +# (a) valid YAML + (b) msan include entry with mirrored keys. +# Both are structural, so PyYAML parses the file and inspects the matrix. +# Exit code is captured explicitly (rather than a plain `if ! ... ; then`) +# so py_rc == 10 (PyYAML not importable) can be told apart from any other +# nonzero code and reported as a SKIP rather than a FAIL. +py_rc=0 +python3 - "$WF" <<'PY' || py_rc=$? +import sys +try: + import yaml +except ImportError: + print(" PyYAML not importable — install with 'pip install pyyaml'", file=sys.stderr) + sys.exit(10) + +path = sys.argv[1] +try: + doc = yaml.safe_load(open(path)) +except Exception as e: # broad catch: surface any YAML parse error to stderr + print(f" (a) YAML parse error: {e}", file=sys.stderr) + sys.exit(2) + +# Locate the matrix include list in whichever job defines it (job name is +# not hard-coded so the gate survives a rename). +include = None +for job_name, job in (doc.get("jobs") or {}).items(): + if not isinstance(job, dict): + continue + inc = (((job.get("strategy") or {}).get("matrix") or {}).get("include")) + if isinstance(inc, list): + include = inc + break +if include is None: + print(" (b) could not locate strategy.matrix.include in any job", file=sys.stderr) + sys.exit(3) + +msan = [e for e in include if isinstance(e, dict) and e.get("build-type") == "msan"] +if not msan: + print(" (b) no matrix include entry with build-type: msan", file=sys.stderr) + sys.exit(4) +if len(msan) > 1: + print(f" (b) expected exactly one msan include entry, found {len(msan)}", file=sys.stderr) + sys.exit(5) +entry = msan[0] + +# Keys mirrored from the asan entry: fixed-value keys must match, and the +# per-lane keys must simply be present (value-agnostic so a future os/compiler +# bump does not break the gate). +fixed = { + "test-group": "extra", + "compiler-family": "clang", + "debug": "debug", + "coverage": "nocoverage", + "shell": "bash", +} +present = ("os", "os-type", "c-compiler", "cc-compiler") +problems = [] +for k, v in fixed.items(): + if entry.get(k) != v: + problems.append(f"{k}={entry.get(k)!r} (want {v!r})") +for k in present: + if not entry.get(k): + problems.append(f"{k} missing") +if problems: + print(" (b) msan include entry missing/incorrect keys: " + ", ".join(problems), file=sys.stderr) + sys.exit(6) + +print(f" (a) YAML parses; (b) msan include entry present with mirrored keys " + f"(os={entry['os']}, c-compiler={entry['c-compiler']})") +PY + +if [ "$py_rc" -eq 10 ]; then + echo " (a)/(b) SKIP — PyYAML unavailable (pip install pyyaml)" +elif [ "$py_rc" -ne 0 ]; then + echo " (a)/(b) FAILED" >&2 + fail=1 +fi + +# (c) The stale ubuntu-18.04 / clang-6.0 commented msan block must be gone. +# Look for a commented-out matrix entry that pairs a msan build-type with the +# retired ubuntu-18.04 / clang-6.0 toolchain. +# NOTE: this heuristic is intentionally coarse -- it matches ANY commented +# line containing one of the two retired strings anywhere in the file, not +# specifically a commented-out msan matrix entry. Good enough to catch the +# known-stale block; a future maintainer should not assume it is precise. +if grep -qE '^[[:space:]]*#.*(ubuntu-18\.04|clang-6\.0)' "$WF"; then + echo " (c) stale commented msan block (ubuntu-18.04 / clang-6.0) is still present" >&2 + fail=1 +else + echo " (c) stale ubuntu-18.04 / clang-6.0 commented msan block removed" +fi + +# (d) The "Run tests" step must wire MSAN_OPTIONS and a scoped TESTS= run. +if grep -q 'MSAN_OPTIONS' "$WF"; then + echo " (d1) MSAN_OPTIONS wiring present" +else + echo " (d1) MSAN_OPTIONS wiring absent from the workflow" >&2 + fail=1 +fi +if grep -qE 'make check TESTS=' "$WF"; then + echo " (d2) scoped 'make check TESTS=' invocation present" +else + echo " (d2) scoped 'make check TESTS=' invocation absent from the workflow" >&2 + fail=1 +fi + +if [ "$fail" -ne 0 ]; then + echo "check-msan-lane: FAIL — the msan lane is not fully wired" >&2 + exit 1 +fi + +echo "check-msan-lane: PASS — msan lane structurally wired" diff --git a/scripts/check-no-tautological-asserts.sh b/scripts/check-no-tautological-asserts.sh new file mode 100755 index 000000000..4b97c9c2c --- /dev/null +++ b/scripts/check-no-tautological-asserts.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +# +# TASK-076 regression gate: forbid `LT_CHECK_EQ(1, 1)` in +# test/integ/ws_start_stop.cpp. +# +# The pattern was used as a fake "skip" before LT_SKIP / LT_SKIP_IF +# existed in test/littletest.hpp. A build that silently lost TLS support +# would still report green because every catch block in the TLS suite +# swallowed the exception with a tautological `LT_CHECK_EQ(1, 1)`. +# TASK-076 replaced all 33 occurrences with either: +# (a) `LT_SKIP_IF(cond, reason)` for environment-conditional skips +# (e.g. gnutls-cli absent from PATH); or +# (b) a real postcondition assertion / typed catch + re-throw for +# cases where the implementation should be exercised; or +# (c) deletion of redundant tail-position liveness pseudo-asserts. +# +# This gate keeps the regression visible by failing if any future patch +# reintroduces the pattern in ws_start_stop.cpp. The scope is +# intentionally narrow — `test/integ/basic.cpp:1877` and +# `test/unit/no_v1_compat_shim_test.cpp:96` both use the same textual +# pattern as intentional liveness/compile-sentinel assertions and are +# NOT covered by this gate (the task wording scopes only to +# ws_start_stop.cpp). +# + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +FILE="$REPO_ROOT/test/integ/ws_start_stop.cpp" + +if [ ! -f "$FILE" ]; then + echo "ERROR: $FILE missing" >&2 + exit 1 +fi + +if grep -nE 'LT_CHECK_EQ\(1, *1\)' "$FILE"; then + echo "ERROR: LT_CHECK_EQ(1, 1) found in $FILE." >&2 + echo " Use LT_SKIP_IF(...) for environment-conditional skips" >&2 + echo " (see TASK-076). The tautological assertion masks broken" >&2 + echo " TLS builds as PASS instead of SKIP." >&2 + exit 1 +fi + +echo "PASS — no tautological LT_CHECK_EQ(1, 1) in $(realpath --relative-to="$REPO_ROOT" "$FILE" 2>/dev/null || echo "$FILE")" diff --git a/scripts/check-readme.sh b/scripts/check-readme.sh new file mode 100755 index 000000000..f5daef67f --- /dev/null +++ b/scripts/check-readme.sh @@ -0,0 +1,359 @@ +#!/usr/bin/env bash +# +# check-readme.sh — enforce TASK-041 invariants on README.md. +# +# This is a static-source check, mirroring scripts/check-examples.sh. It asserts +# that the v2.0 README: +# +# A1. Quotes examples/hello_world.cpp byte-for-byte in its first ```cpp fence. +# A2. Contains no v1-era tokens (sweet_kill, *_response subclasses, no_* setters, +# render_GET-style virtuals, ban_ip/unban_ip/disallow_ip, raw-pointer registration). +# A3. Mentions every load-bearing v2 surface (on_get, register_path, http_response::*, +# deny_ip/allow_ip, stop_and_wait, features(), feature_unavailable, ...). +# A4. The Threading and Error-propagation sections cite their architecture sources +# (DR-008/§5.1 and DR-009/§5.2) and mention the load-bearing details +# (stop() deadlock per DR-008; internal_error_handler + feature_unavailable). +# A5. The eleven structural sections from TASK-041 exist (case-insensitive H2 match). +# A6. Cross-links to examples/ and RELEASE_NOTES.md exist. +# A6b. Every relative Markdown link resolves to a file on disk. +# +# Plus markdown sanity: +# - balanced ``` fences; +# - exactly one top-level `# ` H1; +# - no tab characters inside fenced code blocks. +# +# Exits non-zero on the first violation. + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +README="$REPO_ROOT/README.md" +HELLO="$REPO_ROOT/examples/hello_world.cpp" + +fail() { + echo "check-readme: FAIL: $*" >&2 + exit 1 +} + +[ -f "$README" ] || fail "README.md does not exist at $README" +[ -f "$HELLO" ] || fail "examples/hello_world.cpp does not exist at $HELLO" + +# ---- A1: hello-world snippet matches examples/hello_world.cpp ---------------- +# Extract the first fenced ```cpp ... ``` block from README.md. + +extract_first_cpp_block() { + awk ' + BEGIN { in_block = 0; printed = 0 } + { + gsub(/\r/, "") # strip CRLF line endings (mirrors check-examples.sh) + if (printed) next + if (!in_block && $0 ~ /^```cpp[[:space:]]*$/) { in_block = 1; next } + if (in_block && $0 ~ /^```[[:space:]]*$/) { in_block = 0; printed = 1; next } + if (in_block) print + } + ' "$1" +} + +tmp_block="$(mktemp)" +tmp_hello="$(mktemp)" +trap 'rm -f "$tmp_block" "$tmp_hello"' EXIT +extract_first_cpp_block "$README" | tr -d '\r' > "$tmp_block" +# Strip CRLFs from the cpp file too: msys2/MINGW Windows runners check out +# README.md with CRLF (git autocrlf=true) and the .cpp with LF, which makes +# the byte-for-byte diff fail even though the visible content is identical. +tr -d '\r' < "$HELLO" > "$tmp_hello" + +if [ ! -s "$tmp_block" ]; then + fail "A1: README.md contains no \`\`\`cpp fenced block (need first block to match $HELLO byte-for-byte)" +fi + +if ! diff -u "$tmp_hello" "$tmp_block" >/dev/null 2>&1; then + diff -u "$tmp_hello" "$tmp_block" >&2 || true + fail "A1: first \`\`\`cpp block in README.md does not match examples/hello_world.cpp byte-for-byte" +fi + +# ---- A2: no v1-era tokens ---------------------------------------------------- +# Use a single grep -nE pass over README.md. Any hit fails. + +# The full v1 no_* setter family is enumerated from the shared data file +# scripts/lib/v1-no-setters.txt (same source of truth as check-release-notes.sh) +# so no removed setter can silently leak back into the v2 README. +NO_SETTERS_FILE="$REPO_ROOT/scripts/lib/v1-no-setters.txt" +[ -f "$NO_SETTERS_FILE" ] || fail "A2: enumeration file missing: $NO_SETTERS_FILE" +V1_NO_SETTERS=() +while IFS= read -r _name || [ -n "$_name" ]; do + case "$_name" in + ''|\#*) continue ;; # skip blank lines and # comments + esac + V1_NO_SETTERS+=("\\b${_name}\\b") +done < "$NO_SETTERS_FILE" +[ "${#V1_NO_SETTERS[@]}" -gt 0 ] || fail "A2: $NO_SETTERS_FILE enumerated no setter names" + +V1_TOKENS_ARR=( + '\bsweet_kill\b' + '\bban_ip\b' + '\bunban_ip\b' + '\bdisallow_ip\b' + "${V1_NO_SETTERS[@]}" + '\brender_(GET|POST|PUT|DELETE|HEAD|OPTIONS|PATCH|CONNECT|TRACE)\b' + '\bstring_response\b' + '\bfile_response\b' + '\biovec_response\b' + '\bpipe_response\b' + '\bdeferred_response\b' + '\bempty_response\b' + '\bbasic_auth_fail_response\b' + '\bdigest_auth_fail_response\b' + 'new[[:space:]]+[A-Za-z_]*_response[[:space:]]*\(' + '\bregister_resource\b' + '\bnot_found_resource\b' + '\bmethod_not_allowed_resource\b' + '\binternal_error_resource\b' +) +# Join array entries with | for a single grep pass. +V1_TOKENS="${V1_TOKENS_ARR[0]}" +for _tok in "${V1_TOKENS_ARR[@]:1}"; do V1_TOKENS="$V1_TOKENS|$_tok"; done + +if hits="$(grep -nE "$V1_TOKENS" "$README")"; then + echo "check-readme: FAIL: A2: README.md contains v1-era tokens:" >&2 + echo "$hits" >&2 + exit 1 +fi + +# ---- A3: required v2 tokens appear at least once ----------------------------- +# Each pattern must match. Collect misses and report them all in one go. + +REQUIRED_V2_TOKENS=( + '\bon_get\b' + '\bon_post\b' + '\broute[[:space:]]*\(' + '\bregister_path\b' + '\bregister_prefix\b' + '\bregister_ws_resource\b' + '\bhttp_response::string\b' + '\bhttp_response::file\b' + '\bhttp_response::iovec\b' + '\bhttp_response::pipe\b' + '\bhttp_response::empty\b' + '\bhttp_response::deferred\b' + '\bhttp_response::unauthorized\b' + '\bwith_header\b' + '\bwith_status\b' + '\bdeny_ip\b' + '\bremove_denied_ip\b' + '\ballow_ip\b' + '\bremove_allowed_ip\b' + '\bstop_and_wait\b' + '\bfeatures\(\)' + '\bfeature_unavailable\b' + '\binternal_error_handler\b' + '\bnot_found_handler\b' + '\bmethod_not_allowed_handler\b' + '\bhttp_method\b' + '\bmethod_set\b' + '\biovec_entry\b' + '\badd_hook\b' + '\bhook_phase\b' + '\bhook_action\b' + '\bhook_handle\b' +) + +missing=() +for tok in "${REQUIRED_V2_TOKENS[@]}"; do + if ! grep -qE "$tok" "$README"; then + missing+=("$tok") + fi +done +if [ "${#missing[@]}" -gt 0 ]; then + echo "check-readme: FAIL: A3: README.md is missing required v2 tokens:" >&2 + for tok in "${missing[@]}"; do echo " $tok" >&2; done + exit 1 +fi + +# ---- A4: threading & error sections cite architecture + key terms ----------- +# Find each section body (between its H2 heading and the next H2/EOF) and grep. + +extract_section_body() { + # $1 = file, $2 = regex matching the H2 heading line + awk -v re="$2" ' + BEGIN { in_section = 0 } + /^##[ \t]+/ { + if (in_section) { exit } + if (tolower($0) ~ tolower(re)) { in_section = 1; next } + } + in_section { print } + ' "$1" +} + +thread_body="$(extract_section_body "$README" '^##[ \t]+.*threading')" +err_body="$(extract_section_body "$README" '^##[ \t]+.*error[ \t-]+propag')" + +if [ -z "$thread_body" ]; then + fail "A4: README.md is missing a '## Threading...' section" +fi +if ! echo "$thread_body" | grep -qE '(DR-008|§[[:space:]]*5\.1|specs/architecture/05-cross-cutting\.md|specs/architecture/11-decisions/DR-008)'; then + fail "A4: Threading section must cite DR-008 or §5.1" +fi +if ! echo "$thread_body" | grep -qE '\bstop\(\)'; then + fail "A4: Threading section must mention stop() (the stop-from-handler deadlock, per DR-008)" +fi +if ! echo "$thread_body" | grep -qiE 'deadlock'; then + fail "A4: Threading section must mention the deadlock contract" +fi + +if [ -z "$err_body" ]; then + fail "A4: README.md is missing a '## Error propagation' section" +fi +if ! echo "$err_body" | grep -qE '(DR-009|§[[:space:]]*5\.2|specs/architecture/05-cross-cutting\.md|specs/architecture/11-decisions/DR-009)'; then + fail "A4: Error-propagation section must cite DR-009 or §5.2" +fi +if ! echo "$err_body" | grep -qE '\binternal_error_handler\b'; then + fail "A4: Error-propagation section must mention internal_error_handler" +fi +if ! echo "$err_body" | grep -qE '\bfeature_unavailable\b'; then + fail "A4: Error-propagation section must mention feature_unavailable" +fi + +# ---- A5: required structural sections exist --------------------------------- + +REQUIRED_SECTIONS=( + 'build.*install' + 'hello.*world' + 'class-form' + 'request' + 'response' + 'routing' + 'lifecycle[ \t-]+hook' + 'threading' + 'error[ \t-]+propag' + 'feature[ \t-]+avail' + 'websocket' + 'migrat' +) + +missing_sections=() +for sec in "${REQUIRED_SECTIONS[@]}"; do + if ! grep -qiE "^##[ \t]+.*${sec}" "$README"; then + missing_sections+=("$sec") + fi +done +if [ "${#missing_sections[@]}" -gt 0 ]; then + echo "check-readme: FAIL: A5: README.md is missing required ## sections:" >&2 + for sec in "${missing_sections[@]}"; do echo " pattern: $sec" >&2; done + exit 1 +fi + +# ---- A4b: Lifecycle hooks section content (TASK-052) ----------------------- +# The new "## Lifecycle hooks" section must mention the five v1 aliases +# alongside the word "alias", and reference all four hook examples. + +hooks_body="$(extract_section_body "$README" '^##[ \t]+.*lifecycle[ \t-]+hook')" +if [ -z "$hooks_body" ]; then + fail "A4b: README.md is missing a '## Lifecycle hooks' section" +fi +if ! echo "$hooks_body" | grep -qiE '\balias\b'; then + fail "A4b: Lifecycle hooks section must call out v1 setters as aliases" +fi +for alias_name in log_access not_found_handler method_not_allowed_handler internal_error_handler auth_handler; do + if ! echo "$hooks_body" | grep -qE "\\b${alias_name}\\b"; then + fail "A4b: Lifecycle hooks section must mention v1 alias '${alias_name}'" + fi +done +for example_base in denied_ip_log early_413 clf_access_log per_route_auth; do + if ! echo "$hooks_body" | grep -qE "${example_base}\\.cpp"; then + fail "A4b: Lifecycle hooks section must reference example ${example_base}.cpp" + fi +done + +# ---- A6: cross-links to examples/ and RELEASE_NOTES.md ---------------------- + +if ! grep -qE '\]\(examples/' "$README"; then + fail "A6: README.md must contain at least one Markdown link to examples/ (e.g., [examples/](examples/) or [foo](examples/foo.cpp))" +fi +if ! grep -qE '\]\(RELEASE_NOTES\.md' "$README"; then + fail "A6: README.md must contain at least one Markdown link to RELEASE_NOTES.md" +fi + +# ---- A6b: relative Markdown links resolve to existing files ----------------- +# Extract Markdown link targets of the form ]() where the target +# looks like a file path (no spaces, no C++ keywords). Skip http/https URLs +# and in-page anchors (#...). Verify each relative target exists on disk. +broken_links=() +# Note: grep exit-1 (no matches) is not propagated from process substitution under bash set -e; +# this is an intentional reliance on this bash-specific behavior (the script requires bash already). +while IFS= read -r target; do + case "$target" in + http://*|https://*) continue ;; # absolute URLs — not checked + \#*) continue ;; # in-page anchors — not checked + esac + # Strip fragment identifiers (#section) before existence check to avoid + # false failures when a link like [foo](examples/foo.cpp#line) is added. + target="${target%%#*}" + # Reject path-traversal targets rather than testing them against the + # filesystem: README.md content could originate from an external PR, and + # joining an untrusted "../../../etc/shadow"-style target directly to + # REPO_ROOT would let this check probe paths outside the repo tree. + case "$target" in + *../*|../*) continue ;; + esac + if [ ! -e "$REPO_ROOT/$target" ]; then + broken_links+=("$target") + fi +done < <(grep -oE '\]\([^) ]+\)' "$README" | sed 's/^](//;s/)$//') + +if [ "${#broken_links[@]}" -gt 0 ]; then + echo "check-readme: FAIL: A6b: README.md contains relative links to non-existent files:" >&2 + for lnk in "${broken_links[@]}"; do echo " $lnk" >&2; done + exit 1 +fi + +# ---- Markdown sanity -------------------------------------------------------- +# (S1) Balanced ``` fences, verified by an ordered open/close state machine +# (scripts/lib/check-fence-balance.sh). This replaces the earlier `count % 2` +# parity heuristic, which silently accepted two consecutive opening fences +# plus two closing fences (four lines, even) as "balanced". +if ! "$(dirname "$0")/lib/check-fence-balance.sh" "$README"; then + fail "S1: README.md has unbalanced \`\`\` fences (see diagnostic above)" +fi + +# (S2) Exactly one top-level `# ` H1 line. +h1_count="$(grep -cE '^#[[:space:]]' "$README" || true)" +if [ "$h1_count" -ne 1 ]; then + fail "S2: README.md must have exactly one H1 (\`# \`) heading, found $h1_count" +fi + +# (S3) No literal tab characters inside fenced code blocks. +# Note: this uses the simple open/close toggle (not the S1 ordered state +# machine in lib/check-fence-balance.sh) — S3 only scans for tabs inside +# whatever it thinks is a block, it does not gate structural balance (S1 +# already does that), so the weaker toggle model is intentionally retained +# here. +awk ' + BEGIN { in_block = 0; bad = 0 } + /^```/ { in_block = !in_block; next } + in_block && /\t/ { print NR": "$0; bad = 1 } + END { exit bad } +' "$README" >&2 || fail "S3: fenced code blocks in README.md contain tab characters (must use spaces)" + +# (S4) No CRLF line endings in README.md. +# The grep -P below requires a POSIX-extended grep that understands \r; on macOS +# use LC_ALL=C grep -P to avoid locale issues. +if LC_ALL=C grep -qP '\r$' "$README" 2>/dev/null; then + fail "S4: README.md contains CRLF line endings; convert to LF before committing" +fi + +# ---- markdownlint: strict by default ---------------------------------------- +# A markdownlint finding fails the script by default. Set +# LIBHTTPSERVER_MARKDOWNLINT_ADVISORY=1 to downgrade findings to advisory +# (non-gating) — the documented escape hatch for a lane whose markdownlint +# version flags rules the docs do not yet satisfy. The legacy +# MARKDOWNLINT_STRICT knob is still honored (default now 'yes'); +# MARKDOWNLINT_STRICT=no also downgrades to advisory. See +# scripts/lib/check-markdownlint.sh for the shared decision logic (also used +# by check-release-notes.sh). +# shellcheck source=scripts/lib/check-markdownlint.sh +source "$(dirname "$0")/lib/check-markdownlint.sh" +run_markdownlint_advisory "check-readme" "$README" + +echo "check-readme: OK (A1 byte-for-byte snippet; A2 no v1 tokens; A3 ${#REQUIRED_V2_TOKENS[@]} v2 tokens; A4 threading+error citations; A5 ${#REQUIRED_SECTIONS[@]} sections; A6 cross-links; fences balanced)" +exit 0 diff --git a/scripts/check-release-notes.sh b/scripts/check-release-notes.sh new file mode 100755 index 000000000..41b350d53 --- /dev/null +++ b/scripts/check-release-notes.sh @@ -0,0 +1,394 @@ +#!/usr/bin/env bash +# +# check-release-notes.sh — enforce TASK-042 invariants on RELEASE_NOTES.md. +# +# Like scripts/check-readme.sh, this is a static-source check. It asserts that +# RELEASE_NOTES.md is a complete v1→v2.0 porting summary: +# +# A1. RELEASE_NOTES.md exists at REPO_ROOT. +# A2. Required v1-era tokens appear at least once (this is the inverse of +# check-readme.sh A2 — the rename/removal source list MUST be present +# so v1 users can grep for any old name). +# A3. Required v2-era tokens appear at least once (reuses the +# REQUIRED_V2_TOKENS list from check-readme.sh so the two checks agree). +# A4. Required H2 sections are present: TL;DR, What's gone, What's new, +# What's renamed, What changed semantically, Build prerequisites, +# SOVERSION (and packaging). +# A5. Rename-completeness: each high-value (v1 → v2) pair must appear on +# the SAME LINE (typically a Markdown table row). This is the +# load-bearing acceptance check — "a v1 user can grep for any v1 +# method name and find what replaced it." +# A6. Threading & error sub-sections cite the architecture sources +# (DR-008 / §5.1 and DR-009 / §5.2), mirroring check-readme.sh's A4. +# A7. An explicit "not a compatibility commitment" disclaimer is present +# (case-insensitive). +# +# Plus markdown sanity (S1–S3): +# S1. Balanced ``` fences (count is even). +# S2. Exactly one top-level `# ` H1 line. +# S3. No tab characters inside fenced code blocks. +# +# This script assumes LF-only line endings. Repo's .gitattributes / +# .editorconfig should enforce this; if RELEASE_NOTES.md is ever saved with +# CRLF, the A1/A5 string matches and table-row extractor will silently see +# a trailing \r on every line, which will surface as confusing pair-not-found +# failures rather than a clear "CRLF detected" error. A dedicated CRLF guard +# near A1 catches this early with an explicit diagnostic. +# +# Exits non-zero on the first violation. + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +NOTES="$REPO_ROOT/RELEASE_NOTES.md" + +# RELEASE_NOTES.md content invariants are platform-independent and are fully +# enforced on the Linux and macOS lanes. The token matching relies on GNU/POSIX +# shell tooling (grep word-boundaries and CRLF-free reads): under the MSYS2/mingw +# shell, git autocrlf checks the file/source lists out with CRLF, which taints +# the v1-token list (each token gains an embedded \r) so nothing matches. Skip on +# Windows rather than re-validate identical repo content a third time. +case "$(uname -s)" in + MINGW*|MSYS*|CYGWIN*) + echo "check-release-notes: SKIP on Windows/MSYS (content gate enforced on POSIX lanes)" + exit 0 + ;; +esac + +fail() { + echo "check-release-notes: FAIL: $*" >&2 + exit 1 +} + +# Helper: check that every token in the given array appears at least once in +# the target file. Usage: check_tokens_present