fix(ja4ebpf): split bpf2go generate into Ja4Tc + Ja4Ssl, fix RPM systemd-rpm-macros

- Use two separate //go:generate directives (Ja4Tc for tc_capture.c, Ja4Ssl
  for uprobe_ssl.c) to avoid duplicate LICENSE symbol and multi-file clang issue
- Update loader.go to hold tcObjs/sslObjs separately with correct field names:
  UprobeSslSetFd, UprobeSslReadEntry, UretprobeSslReadExit,
  KprobeAccept4Entry, KretprobeAccept4Exit
- Add systemd-rpm-macros to all three RPM build stages (el8/el9/el10)
  so that %{_unitdir} macro resolves correctly
- RPMs now build successfully for el8, el9, el10

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
toto
2026-04-11 23:21:11 +02:00
parent a1e4c1dad5
commit 3b047b680a
155 changed files with 197011 additions and 599 deletions

35
old/services/mod-reqin-log/.gitignore vendored Normal file
View File

@ -0,0 +1,35 @@
# Build artifacts
*.o
*.so
*.a
*.la
.deps
.libs
# Build directories
build/
cmake-build-*/
dist/
bin/
obj/
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# Logs
*.log
# Testing
coverage/
*.gcno
*.gcda
# Packaging
*.rpm
*.deb
*.tar.gz
.aider*

View File

@ -0,0 +1,94 @@
# GitLab CI/CD configuration for mod_reqin_log
# Uses Docker-in-Docker (dind) for building and testing
stages:
- build
- test
- package
- verify
# =============================================================================
# Variables
# =============================================================================
variables:
DOCKER_TLS_CERTDIR: "/certs"
DOCKER_DRIVER: overlay2
VERSION: "1.0.2"
# =============================================================================
# Build Stage - Compile all RPM packages
# =============================================================================
build-packages:
stage: build
image: docker:24
services:
- docker:24-dind
script:
# Build all RPM packages (el8, el9, el10)
- docker build -f Dockerfile.package --target output --build-arg VERSION=$VERSION -t mod_reqin_log:packages .
# Create output directories
- mkdir -p dist/rpm
# Extract packages from Docker image
- docker run --rm -v $(pwd)/dist:/output mod_reqin_log:packages sh -c 'cp -r /packages/rpm/* /output/rpm/'
# List built packages
- echo "=== RPM Packages ==="
- ls -la dist/rpm/
artifacts:
paths:
- dist/rpm/
expire_in: 30 days
# =============================================================================
# Test Stage - Unit tests
# =============================================================================
unit-tests:
stage: test
image: docker:24
services:
- docker:24-dind
script:
# Build test image
- docker build -f Dockerfile.tests -t mod_reqin_log:tests .
# Run unit tests
- docker run --rm mod_reqin_log:tests ctest --output-on-failure
# =============================================================================
# Package Stage - Already done in build-packages
# =============================================================================
# =============================================================================
# Verify Stage - Test RPM package installation on each target distribution
# =============================================================================
verify-rpm-el8:
stage: verify
image: docker:24
services:
- docker:24-dind
needs: [build-packages]
script:
- docker run --rm -v $(pwd)/dist:/packages rockylinux:8 sh -c "dnf install -y /packages/rpm/*.el8.*.rpm && httpd -M 2>&1 | grep reqin_log && echo 'RPM el8 verification OK'"
verify-rpm-el9:
stage: verify
image: docker:24
services:
- docker:24-dind
needs: [build-packages]
script:
- docker run --rm -v $(pwd)/dist:/packages rockylinux:9 sh -c "dnf install -y /packages/rpm/*.el9.*.rpm && httpd -M 2>&1 | grep reqin_log && echo 'RPM el9 verification OK'"
verify-rpm-el10:
stage: verify
image: docker:24
services:
- docker:24-dind
needs: [build-packages]
script:
- docker run --rm -v $(pwd)/dist:/packages almalinux:10 sh -c "dnf install -y /packages/rpm/*.el10.*.rpm && httpd -M 2>&1 | grep reqin_log && echo 'RPM el10 verification OK'"

View File

@ -0,0 +1,47 @@
cmake_minimum_required(VERSION 3.10)
project(mod_reqin_log_tests C)
set(CMAKE_C_STANDARD 99)
set(CMAKE_C_STANDARD_REQUIRED ON)
# Find required packages
find_package(PkgConfig REQUIRED)
pkg_check_modules(CMOCKA REQUIRED cmocka)
pkg_check_modules(APR REQUIRED apr-1)
pkg_check_modules(APRUTIL REQUIRED apr-util-1)
# Include directories
include_directories(${CMOCKA_INCLUDE_DIRS})
include_directories(${APR_INCLUDE_DIRS})
include_directories(${APRUTIL_INCLUDE_DIRS})
include_directories(/usr/include/httpd)
# Test executable - Real module tests (testing actual implementation)
add_executable(test_module_real tests/unit/test_module_real.c)
target_link_libraries(test_module_real ${CMOCKA_LIBRARIES} ${APR_LIBRARIES} ${APRUTIL_LIBRARIES} m)
add_executable(test_config_parsing tests/unit/test_config_parsing.c)
target_link_libraries(test_config_parsing ${CMOCKA_LIBRARIES})
add_executable(test_header_handling tests/unit/test_header_handling.c)
target_link_libraries(test_header_handling ${CMOCKA_LIBRARIES} ${APR_LIBRARIES})
add_executable(test_json_serialization tests/unit/test_json_serialization.c)
target_link_libraries(test_json_serialization ${CMOCKA_LIBRARIES} ${APR_LIBRARIES})
add_executable(test_h2_parsing tests/unit/test_h2_parsing.c)
target_link_libraries(test_h2_parsing ${CMOCKA_LIBRARIES})
# Enable testing
enable_testing()
add_test(NAME RealModuleTest COMMAND test_module_real)
add_test(NAME ConfigParsingTest COMMAND test_config_parsing)
add_test(NAME HeaderHandlingTest COMMAND test_header_handling)
add_test(NAME JsonSerializationTest COMMAND test_json_serialization)
add_test(NAME H2ParsingTest COMMAND test_h2_parsing)
# Custom target for running tests
add_custom_target(run_tests
COMMAND ${CMAKE_CTEST_COMMAND} --output-on-failure
DEPENDS test_module_real test_config_parsing test_header_handling test_json_serialization test_h2_parsing
)

View File

@ -0,0 +1,176 @@
# syntax=docker/dockerfile:1
# =============================================================================
# mod_reqin_log - Dockerfile de packaging RPM
# Builds RPMs for multiple RHEL-compatible versions:
# - Rocky Linux 8 (el8) - RHEL 8 compatible
# - Rocky Linux 9 (el9) - RHEL 9 compatible
# - AlmaLinux 10 (el10) - RHEL 10 compatible
# =============================================================================
# =============================================================================
# Stage 1a: Builder Rocky Linux 8
# =============================================================================
FROM rockylinux:8 AS builder-el8
RUN dnf install -y epel-release && \
dnf install -y --allowerasing \
gcc \
make \
httpd \
httpd-devel \
apr-devel \
apr-util-devel \
python3 \
curl \
redhat-rpm-config \
&& dnf clean all
WORKDIR /build
COPY services/mod-reqin-log/src/ src/
COPY services/mod-reqin-log/Makefile Makefile
COPY services/mod-reqin-log/conf/ conf/
RUN make APXS=/usr/bin/apxs
RUN ls -la modules/mod_reqin_log.so
# =============================================================================
# Stage 1b: Builder Rocky Linux 9
# =============================================================================
FROM rockylinux:9 AS builder-el9
RUN dnf install -y epel-release && \
dnf install -y --allowerasing \
gcc \
make \
httpd \
httpd-devel \
apr-devel \
apr-util-devel \
python3 \
curl \
redhat-rpm-config \
&& dnf clean all
WORKDIR /build
COPY services/mod-reqin-log/src/ src/
COPY services/mod-reqin-log/Makefile Makefile
COPY services/mod-reqin-log/conf/ conf/
RUN make APXS=/usr/bin/apxs
RUN ls -la modules/mod_reqin_log.so
# =============================================================================
# Stage 1c: Builder AlmaLinux 10 (RHEL 10 compatible)
# =============================================================================
FROM almalinux:10 AS builder-el10
RUN dnf install -y epel-release && \
dnf install -y --allowerasing \
gcc \
make \
httpd \
httpd-devel \
apr-devel \
apr-util-devel \
python3 \
curl \
redhat-rpm-config \
&& dnf clean all
WORKDIR /build
COPY services/mod-reqin-log/src/ src/
COPY services/mod-reqin-log/Makefile Makefile
COPY services/mod-reqin-log/conf/ conf/
RUN make APXS=/usr/bin/apxs
RUN ls -la modules/mod_reqin_log.so
# =============================================================================
# Stage 2: Package builder - rpmbuild pour RPM
# =============================================================================
FROM rockylinux:9 AS package-builder
WORKDIR /package
# Install rpm-build and dependencies
RUN dnf install -y rpm-build rpmdevtools && \
dnf clean all
# Create rpmbuild directory structure
RUN rpmdev-setuptree
# =============================================================================
# Copy spec file and source files
# =============================================================================
COPY services/mod-reqin-log/mod_reqin_log.spec /package/mod_reqin_log.spec
# =============================================================================
# Copy binaries from each builder stage into pkgroot directories
# =============================================================================
# Rocky Linux 8 (el8)
COPY --from=builder-el8 /build/modules/mod_reqin_log.so /tmp/pkgroot-el8/usr/lib64/httpd/modules/mod_reqin_log.so
COPY --from=builder-el8 /build/conf/mod_reqin_log.conf /tmp/pkgroot-el8/etc/httpd/conf.d/mod_reqin_log.conf
RUN chmod 755 /tmp/pkgroot-el8/usr/lib64/httpd/modules/mod_reqin_log.so && \
chmod 644 /tmp/pkgroot-el8/etc/httpd/conf.d/mod_reqin_log.conf
# Rocky Linux 9 (el9)
COPY --from=builder-el9 /build/modules/mod_reqin_log.so /tmp/pkgroot-el9/usr/lib64/httpd/modules/mod_reqin_log.so
COPY --from=builder-el9 /build/conf/mod_reqin_log.conf /tmp/pkgroot-el9/etc/httpd/conf.d/mod_reqin_log.conf
RUN chmod 755 /tmp/pkgroot-el9/usr/lib64/httpd/modules/mod_reqin_log.so && \
chmod 644 /tmp/pkgroot-el9/etc/httpd/conf.d/mod_reqin_log.conf
# AlmaLinux 10 (el10)
COPY --from=builder-el10 /build/modules/mod_reqin_log.so /tmp/pkgroot-el10/usr/lib64/httpd/modules/mod_reqin_log.so
COPY --from=builder-el10 /build/conf/mod_reqin_log.conf /tmp/pkgroot-el10/etc/httpd/conf.d/mod_reqin_log.conf
RUN chmod 755 /tmp/pkgroot-el10/usr/lib64/httpd/modules/mod_reqin_log.so && \
chmod 644 /tmp/pkgroot-el10/etc/httpd/conf.d/mod_reqin_log.conf
# =============================================================================
# Build RPM packages for each distribution using rpmbuild
# =============================================================================
# Create packages directory
RUN mkdir -p /tmp/packages/el8 /tmp/packages/el9 /tmp/packages/el10
# Build for el8
RUN VERSION=$(grep "^Version:" /package/mod_reqin_log.spec | awk '{print $2}') && \
mkdir -p /tmp/pkgroot-el8-rpm/usr/lib64/httpd/modules /tmp/pkgroot-el8-rpm/etc/httpd/conf.d && \
cp /tmp/pkgroot-el8/usr/lib64/httpd/modules/mod_reqin_log.so /tmp/pkgroot-el8-rpm/usr/lib64/httpd/modules/ && \
cp /tmp/pkgroot-el8/etc/httpd/conf.d/mod_reqin_log.conf /tmp/pkgroot-el8-rpm/etc/httpd/conf.d/ && \
rpmbuild -bb /package/mod_reqin_log.spec \
--define "_topdir /tmp/rpmbuild-el8" \
--define "_pkgroot /tmp/pkgroot-el8-rpm" \
--define "dist .el8" && \
cp /tmp/rpmbuild-el8/RPMS/x86_64/*.rpm /tmp/packages/el8/
# Build for el9
RUN VERSION=$(grep "^Version:" /package/mod_reqin_log.spec | awk '{print $2}') && \
mkdir -p /tmp/pkgroot-el9-rpm/usr/lib64/httpd/modules /tmp/pkgroot-el9-rpm/etc/httpd/conf.d && \
cp /tmp/pkgroot-el9/usr/lib64/httpd/modules/mod_reqin_log.so /tmp/pkgroot-el9-rpm/usr/lib64/httpd/modules/ && \
cp /tmp/pkgroot-el9/etc/httpd/conf.d/mod_reqin_log.conf /tmp/pkgroot-el9-rpm/etc/httpd/conf.d/ && \
rpmbuild -bb /package/mod_reqin_log.spec \
--define "_topdir /tmp/rpmbuild-el9" \
--define "_pkgroot /tmp/pkgroot-el9-rpm" \
--define "dist .el9" && \
cp /tmp/rpmbuild-el9/RPMS/x86_64/*.rpm /tmp/packages/el9/
# Build for el10
RUN VERSION=$(grep "^Version:" /package/mod_reqin_log.spec | awk '{print $2}') && \
mkdir -p /tmp/pkgroot-el10-rpm/usr/lib64/httpd/modules /tmp/pkgroot-el10-rpm/etc/httpd/conf.d && \
cp /tmp/pkgroot-el10/usr/lib64/httpd/modules/mod_reqin_log.so /tmp/pkgroot-el10-rpm/usr/lib64/httpd/modules/ && \
cp /tmp/pkgroot-el10/etc/httpd/conf.d/mod_reqin_log.conf /tmp/pkgroot-el10-rpm/etc/httpd/conf.d/ && \
rpmbuild -bb /package/mod_reqin_log.spec \
--define "_topdir /tmp/rpmbuild-el10" \
--define "_pkgroot /tmp/pkgroot-el10-rpm" \
--define "dist .el10" && \
cp /tmp/rpmbuild-el10/RPMS/x86_64/*.rpm /tmp/packages/el10/
# =============================================================================
# Stage 3: Output - Image finale avec les packages RPM
# =============================================================================
FROM alpine:latest AS output
WORKDIR /packages
COPY --from=package-builder /tmp/packages/el8/*.rpm /packages/rpm/el8/
COPY --from=package-builder /tmp/packages/el9/*.rpm /packages/rpm/el9/
COPY --from=package-builder /tmp/packages/el10/*.rpm /packages/rpm/el10/
CMD ["sh", "-c", "echo '=== RPM Packages (el8) ===' && ls -la /packages/rpm/el8/ && echo '' && echo '=== RPM Packages (el9) ===' && ls -la /packages/rpm/el9/ && echo '' && echo '=== RPM Packages (el10) ===' && ls -la /packages/rpm/el10/"]

View File

@ -0,0 +1,43 @@
# Dockerfile for running unit tests (monorepo root build context)
FROM rockylinux:8
# Install build and test dependencies
RUN dnf install -y epel-release && \
dnf install -y \
gcc \
make \
httpd \
httpd-devel \
apr-devel \
apr-util-devel \
cmake \
python3 \
curl \
git \
pkgconfig \
libxml2-devel \
&& dnf clean all
# Build and install cmocka from source
RUN cd /tmp && \
git clone https://git.cryptomilk.org/projects/cmocka.git && \
cd cmocka && \
git checkout cmocka-1.1.5 && \
mkdir build && cd build && \
cmake .. -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=Release && \
make && \
make install && \
ldconfig && \
cd / && \
rm -rf /tmp/cmocka
WORKDIR /build
COPY services/mod-reqin-log/src/ src/
COPY services/mod-reqin-log/tests/ tests/
COPY services/mod-reqin-log/CMakeLists.txt CMakeLists.txt
COPY services/mod-reqin-log/Makefile Makefile
RUN mkdir -p build/tests && cd build/tests && cmake ../../ && make
CMD ["ctest", "--test-dir", "build/tests", "--output-on-failure"]

View File

@ -0,0 +1,107 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction, and
distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by the
copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all other
entities that control, are controlled by, or are under common control with
that entity.
"You" (or "Your") shall mean an individual or Legal Entity exercising
permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation source,
and configuration files.
"Object" form shall mean any form resulting from mechanical transformation
or translation of a Source form, including but not limited to compiled
object code, generated documentation, and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or Object form,
made available under the License.
"Derivative Works" shall mean any work, whether in Source or Object form,
that is based on (or derived from) the Work.
"Contribution" shall mean any work of authorship, including the original
version of the Work and any modifications or additions to that Work or
Derivative Works thereof.
"Contributor" shall mean Licensor and any individual or Legal Entity on
behalf of whom a Contribution has been received by Licensor.
2. Grant of Copyright License.
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the Work and
such Derivative Works in Source or Object form.
3. Grant of Patent License.
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable (except as stated in this section) patent license to make, have
made, use, offer to sell, sell, import, and otherwise transfer the Work.
4. Redistribution.
You may reproduce and distribute copies of the Work or Derivative Works
thereof in any medium, with or without modifications, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or Derivative Works a
copy of this License; and
(b) You must cause any modified files to carry prominent notices stating
that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works that You
distribute, all copyright, patent, trademark, and attribution notices
from the Source form of the Work; and
(d) You may add Your own copyright statement to Your modifications and may
provide additional or different license terms and conditions for use,
reproduction, or distribution of Your modifications.
5. Submission of Contributions.
Unless You explicitly state otherwise, any Contribution intentionally
submitted for inclusion in the Work by You shall be under the terms and
conditions of this License.
6. Trademarks.
This License does not grant permission to use the trade names, trademarks,
service marks, or product names of the Licensor.
7. Disclaimer of Warranty.
Unless required by applicable law or agreed to in writing, Licensor provides
the Work on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
either express or implied.
8. Limitation of Liability.
In no event shall any Contributor be liable to You for damages, including
any direct, indirect, special, incidental, or consequential damages arising
out of the use or inability to use the Work.
9. Accepting Warranty or Additional Liability.
While redistributing the Work or Derivative Works thereof, You may choose
to offer, and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this License.
END OF TERMS AND CONDITIONS

View File

@ -0,0 +1,153 @@
# Makefile for mod_reqin_log
# Apache HTTPD module for logging HTTP requests as JSON to Unix socket
# APXS tool path (can be overridden)
APXS ?= apxs
# Compiler settings
CC ?= gcc
CFLAGS ?= -Wall -Wextra -O2 -std=gnu11 -x c -Wno-error=format-security
# Directories
SRC_DIR = src
BUILD_DIR = build
INSTALL_DIR = modules
DIST_DIR = dist
# Source files
SRCS = $(SRC_DIR)/mod_reqin_log.c
# Module name
MODULE_NAME = mod_reqin_log
# Package version
VERSION ?= 1.0.7
.PHONY: all clean install uninstall test package package-deb package-rpm
all: $(MODULE_NAME).so
# Build the module using apxs
# Note: Use -Wc to pass flags to the C compiler through apxs
$(MODULE_NAME).so: $(SRCS)
@mkdir -p $(BUILD_DIR)
$(APXS) -c -Wc,"$(CFLAGS)" -o $(BUILD_DIR)/$(MODULE_NAME).so $(SRCS)
@mkdir -p $(INSTALL_DIR)
@if [ -f $(BUILD_DIR)/.libs/$(MODULE_NAME).so ]; then \
cp $(BUILD_DIR)/.libs/$(MODULE_NAME).so $(INSTALL_DIR)/; \
elif [ -f $(BUILD_DIR)/$(MODULE_NAME).so ]; then \
cp $(BUILD_DIR)/$(MODULE_NAME).so $(INSTALL_DIR)/; \
fi
# Install the module
install: $(MODULE_NAME).so
@echo "Installing $(MODULE_NAME).so..."
@mkdir -p $(DESTDIR)/usr/lib/apache2/modules
@if [ -f $(INSTALL_DIR)/$(MODULE_NAME).so ]; then \
cp $(INSTALL_DIR)/$(MODULE_NAME).so $(DESTDIR)/usr/lib/apache2/modules/; \
elif [ -f $(BUILD_DIR)/.libs/$(MODULE_NAME).so ]; then \
cp $(BUILD_DIR)/.libs/$(MODULE_NAME).so $(DESTDIR)/usr/lib/apache2/modules/; \
elif [ -f $(BUILD_DIR)/$(MODULE_NAME).so ]; then \
cp $(BUILD_DIR)/$(MODULE_NAME).so $(DESTDIR)/usr/lib/apache2/modules/; \
else \
echo "Error: $(MODULE_NAME).so not found"; \
exit 1; \
fi
@echo "Installation complete."
@echo "Enable the module by adding to your httpd.conf:"
@echo " LoadModule reqin_log_module modules/mod_reqin_log.so"
# Uninstall the module
uninstall:
rm -f $(DESTDIR)/usr/lib/apache2/modules/$(MODULE_NAME).so
@echo "Uninstallation complete."
# Clean build artifacts
clean:
rm -rf $(BUILD_DIR) $(INSTALL_DIR)
rm -f .libs/*.o .libs/*.la .libs/*.so
rm -f *.o *.la *.lo
rm -rf .libs
# Run unit tests (requires cmocka)
test:
@mkdir -p build/tests
cd build/tests && cmake ../../ -DCMAKE_BUILD_TYPE=Debug
$(MAKE) -C build/tests run_tests
# Build with debug symbols
debug: CFLAGS += -g -DDEBUG
debug: clean all
# =============================================================================
# Packaging (RPM with Docker + fpm)
# Dockerfile.package builds RPMs in a single multi-stage build:
# - 3 RPM packages (el8, el9, el10 for RHEL/Rocky/AlmaLinux compatibility)
# =============================================================================
## package: Build all RPM packages (el8, el9, el10)
package:
mkdir -p $(DIST_DIR)/rpm/el8 $(DIST_DIR)/rpm/el9 $(DIST_DIR)/rpm/el10
docker build --target output -t mod_reqin_log:packager \
--build-arg VERSION=$(VERSION) \
-f Dockerfile.package .
@echo "Extracting packages from Docker image..."
docker run --rm -v $(PWD)/$(DIST_DIR)/rpm/el8:/output/el8 \
-v $(PWD)/$(DIST_DIR)/rpm/el9:/output/el9 \
-v $(PWD)/$(DIST_DIR)/rpm/el10:/output/el10 \
mod_reqin_log:packager \
sh -c 'cp /packages/rpm/el8/*.rpm /output/el8/ && cp /packages/rpm/el9/*.rpm /output/el9/ && cp /packages/rpm/el10/*.rpm /output/el10/'
@echo "Packages created:"
@echo " RPM (el8, el9, el10):"
@ls -la $(DIST_DIR)/rpm/el8/
@ls -la $(DIST_DIR)/rpm/el9/
@ls -la $(DIST_DIR)/rpm/el10/
## package-rpm: Build RPM packages (el8, el9, el10)
package-rpm: package
@echo "RPM packages built in Dockerfile.package"
## test-package-rpm: Test RPM package installation in Docker (tests el9 by default)
test-package-rpm: package
docker run --rm -v $(PWD)/$(DIST_DIR)/rpm/el9:/packages:ro rockylinux:9 \
sh -c "dnf install -y /packages/*.el9.*.rpm && echo 'RPM el9 install OK'"
## test-package-rpm-el8: Test el8 RPM installation
test-package-rpm-el8: package
docker run --rm -v $(PWD)/$(DIST_DIR)/rpm/el8:/packages:ro rockylinux:8 \
sh -c "dnf install -y /packages/*.el8.*.rpm && echo 'RPM el8 install OK'"
## test-package-rpm-el9: Test el9 RPM installation
test-package-rpm-el9: package
docker run --rm -v $(PWD)/$(DIST_DIR)/rpm/el9:/packages:ro rockylinux:9 \
sh -c "dnf install -y /packages/*.el9.*.rpm && echo 'RPM el9 install OK'"
## test-package-rpm-el10: Test el10 RPM installation
test-package-rpm-el10: package
docker run --rm -v $(PWD)/$(DIST_DIR)/rpm/el10:/packages:ro almalinux:10 \
sh -c "dnf install -y /packages/*.el10.*.rpm && echo 'RPM el10 install OK'"
## test-package: Test all RPM packages installation
test-package: test-package-rpm-el8 test-package-rpm-el9 test-package-rpm-el10
# Help target
help:
@echo "mod_reqin_log Makefile"
@echo ""
@echo "Targets:"
@echo " all - Build the module (default)"
@echo " install - Install the module to DESTDIR"
@echo " uninstall - Remove the module from DESTDIR"
@echo " clean - Remove build artifacts"
@echo " test - Run unit tests"
@echo " debug - Build with debug symbols"
@echo " package - Build all RPM packages (el8, el9, el10)"
@echo " package-rpm - Build RPM packages"
@echo " test-package - Test RPM package installation"
@echo ""
@echo "Variables:"
@echo " APXS - Path to apxs tool (default: apxs)"
@echo " CC - C compiler (default: gcc)"
@echo " CFLAGS - Compiler flags (default: -Wall -Wextra -O2)"
@echo " DESTDIR - Installation destination (default: /)"
@echo " VERSION - Package version (default: 1.0.4)"

View File

@ -0,0 +1,284 @@
# mod_reqin_log
Apache HTTPD 2.4 module for logging all incoming HTTP requests as JSON lines to a Unix domain socket.
## Features
- **Non-blocking I/O**: Logging never blocks worker processes
- **Request-time logging**: Logs at `post_read_request` phase, capturing request data before application processing
- **Configurable headers**: Select which HTTP headers to include in logs
- **Header truncation**: Limit header value length to protect against oversized logs
- **Automatic reconnection**: Reconnects to Unix socket on failure with configurable backoff
- **Throttled error reporting**: Prevents error_log flooding on persistent failures
- **MPM compatible**: Works with prefork, worker, and event MPMs
- **Built-in security**: Sensitive headers (Authorization, Cookie, etc.) are automatically excluded
- **RPM packaging**: Standard RPM packages for Rocky Linux 8/9 and AlmaLinux 10
## Requirements
### Runtime
- Apache HTTPD 2.4+
- GCC compiler
- APR development libraries
- Apache development headers (`httpd-devel` or `apache2-dev`)
### Packaging (RPM)
- Docker (for reproducible builds)
- rpmbuild (inside Docker)
## Installation
### Using Docker (recommended)
```bash
# Build all RPM packages (el8, el9, el10)
make package
# Test RPM package installation
make test-package-rpm-el8 # Test el8 RPM (Rocky 8/RHEL 8)
make test-package-rpm-el9 # Test el9 RPM (Rocky 9/RHEL 9)
make test-package-rpm-el10 # Test el10 RPM (AlmaLinux 10/RHEL 10)
make test-package # Test all RPM packages
```
### Build from Source
```bash
# Clone or extract the source
cd mod_reqin_log
# Build the module
make
# Install (requires root privileges)
sudo make install
```
## Configuration
Load the module and configure in your Apache configuration:
```apache
# Load the module
LoadModule reqin_log_module modules/mod_reqin_log.so
# Enable logging
JsonSockLogEnabled On
# Unix socket path
JsonSockLogSocket "/var/run/logcorrelator/http.socket"
# Headers to log (be careful not to log sensitive data)
JsonSockLogHeaders X-Request-Id X-Trace-Id User-Agent Referer
# Maximum headers to log
JsonSockLogMaxHeaders 10
# Maximum header value length
JsonSockLogMaxHeaderValueLen 256
# Reconnect interval (seconds)
JsonSockLogReconnectInterval 10
# Error report interval (seconds)
JsonSockLogErrorReportInterval 10
```
> **Important startup validation:** if `JsonSockLogEnabled On` is set without a valid `JsonSockLogSocket`, Apache startup fails with a configuration error.
### Configuration Directives
| Directive | Type | Default | Description |
|-----------|------|---------|-------------|
| `JsonSockLogEnabled` | On/Off | Off | Enable or disable logging |
| `JsonSockLogSocket` | String | - | Unix domain socket path |
| `JsonSockLogHeaders` | List | - | HTTP headers to include |
| `JsonSockLogMaxHeaders` | Integer | 10 | Max headers to log |
| `JsonSockLogMaxHeaderValueLen` | Integer | 256 | Max header value length |
| `JsonSockLogReconnectInterval` | Integer | 10 | Reconnect delay (seconds) |
| `JsonSockLogErrorReportInterval` | Integer | 10 | Error log throttle (seconds) |
## JSON Log Format
Each log entry is a single-line JSON object with a flat structure:
```json
{
"time": "2026-02-26T11:59:30Z",
"timestamp": 1708948770000000000,
"src_ip": "192.0.2.10",
"src_port": 45678,
"dst_ip": "198.51.100.5",
"dst_port": 443,
"method": "GET",
"path": "/api/users",
"host": "example.com",
"http_version": "HTTP/1.1",
"header_X-Request-Id": "abcd-1234",
"header_User-Agent": "curl/7.70.0"
}
```
### Fields
| Field | Type | Description |
|-------|------|-------------|
| `time` | String | ISO8601 timestamp with timezone |
| `timestamp` | Integer | Microseconds since epoch (expressed as nanoseconds for compatibility) |
| `src_ip` | String | Client IP address |
| `src_port` | Integer | Client port |
| `dst_ip` | String | Server IP address |
| `dst_port` | Integer | Server port |
| `method` | String | HTTP method |
| `path` | String | Request path |
| `host` | String | Host header value |
| `http_version` | String | HTTP protocol version |
| `header_<Name>` | String | Flattened HTTP headers (e.g., `header_X-Request-Id`) |
**Note:** Headers are logged as flat fields at the root level (not nested). Sensitive headers are automatically excluded. The `timestamp` field has microsecond precision (APR's `apr_time_now()` returns microseconds, multiplied by 1000 for nanosecond representation).
## Unix Socket Consumer
Create a Unix socket listener to receive log entries:
```python
#!/usr/bin/env python3
import socket
import os
import json
SOCKET_PATH = os.environ.get("MOD_REQIN_LOG_SOCKET", "/var/run/logcorrelator/http.socket")
# Remove existing socket file
if os.path.exists(SOCKET_PATH):
os.remove(SOCKET_PATH)
# Create Unix socket server
server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
server.bind(SOCKET_PATH)
server.listen(5)
# Set secure permissions: owner and group only (not world-writable)
os.chmod(SOCKET_PATH, 0o660)
print(f"Listening on {SOCKET_PATH}")
while True:
conn, addr = server.accept()
data = b""
while True:
chunk = conn.recv(4096)
if not chunk:
break
data += chunk
if b"\n" in data:
for line in data.decode().strip().split("\n"):
if line:
log_entry = json.loads(line)
print(json.dumps(log_entry, indent=2))
data = b""
conn.close()
```
**Note:** Ensure the Apache user is in the socket file's group to allow connections.
## Security Considerations
### Built-in Sensitive Headers Blacklist
⚠️ **The module automatically blocks logging of sensitive headers:**
The following headers are **always excluded** from logs to prevent credential leakage:
- `Authorization`
- `Cookie`, `Set-Cookie`
- `X-Api-Key`, `X-Auth-Token`
- `Proxy-Authorization`
- `WWW-Authenticate`
These headers are silently skipped (logged at DEBUG level only).
### Socket Security
- **Socket permissions**: Default to `0o660` (owner and group only)
- **Recommended path**: `/var/run/logcorrelator/http.socket` (not `/tmp`)
- **Environment variable**: Use `MOD_REQIN_LOG_SOCKET` to configure path
- **Group membership**: Ensure Apache user is in the socket's group
### Additional Hardening
- **Socket path length**: Validated against system limit (108 bytes max)
- **JSON size limit**: 64KB max per log line (prevents memory DoS)
- **NULL pointer checks**: All connection/request fields validated
- **Thread safety**: Mutex protects socket FD in worker/event MPMs
- **Error logging**: Generic messages in error_log, details at DEBUG level
## Troubleshooting
### Module not loading
```
AH00534: mod_reqin_log: Unable to load module
```
Ensure the module path is correct and the file exists:
```bash
ls -la /usr/lib/apache2/modules/mod_reqin_log.so
```
### Socket connection failures
```
[mod_reqin_log] Unix socket connect failed: /var/run/logcorrelator/http.socket
```
- Ensure the socket consumer is running
- Check socket file permissions
- Verify SELinux/AppArmor policies if applicable
### No logs appearing
1. Verify `JsonSockLogEnabled On` is set
2. Verify `JsonSockLogSocket` path is configured
3. Check Apache error log for module errors
4. Ensure socket consumer is listening
## Testing
### Run Unit Tests
```bash
# Using Docker (recommended)
docker build -f Dockerfile.tests -t mod_reqin_log:tests .
docker run --rm mod_reqin_log:tests ctest --output-on-failure
# Or locally with cmocka
sudo dnf install cmocka-devel # Rocky Linux
sudo apt install libcmocka-dev # Debian/Ubuntu
mkdir build && cd build
cmake ..
make test
```
### Build and Test Packages
```bash
# Build all RPM packages (el8, el9, el10)
make package
# Test RPM package installation
make test-package-rpm-el8 # Test el8 RPM in Docker
make test-package-rpm-el9 # Test el9 RPM in Docker
make test-package-rpm-el10 # Test el10 RPM in Docker
make test-package # Test all RPM packages
```
## License
Apache License 2.0
## Contributing
1. Fork the repository
2. Create a feature branch
3. Make your changes
4. Run tests
5. Submit a pull request

View File

@ -0,0 +1,443 @@
project:
name: mod_reqin_log
description: >
Apache HTTPD 2.4 module logging all incoming HTTP requests as JSON lines
to a Unix domain socket at request reception time (no processing time).
language: c
author:
name: Jacquin Antoine
email: rpm@arkel.fr
target:
server: apache-httpd
version: "2.4"
os: rocky-linux-8+, almalinux-10+
build:
toolchain: gcc
apache_dev: httpd-devel (apxs)
artifacts:
- mod_reqin_log.so
context:
architecture:
pattern: native-apache-module
scope: global
mpm_compatibility:
- prefork
- worker
- event
request_phase:
hook: post_read_request
rationale: >
Log as soon as the HTTP request is fully read to capture input-side data
(client/server addresses, request line, headers) without waiting for
application processing.
filters:
- Subrequests (r->main != NULL) are skipped.
- Internal redirects (r->prev != NULL) are skipped.
- Only the original client request is logged.
logging_scope:
coverage: all-traffic
description: >
Every HTTP request handled by the Apache instance is considered for logging
when the module is enabled and the Unix socket is configured.
module:
name: mod_reqin_log
files:
source:
- src/mod_reqin_log.c
- src/mod_reqin_log.h
packaging:
- mod_reqin_log.spec
tests:
- tests/unit/test_module_real.c
- tests/unit/test_config_parsing.c
- tests/unit/test_header_handling.c
- tests/unit/test_json_serialization.c
hooks:
- name: register_hooks
responsibilities:
- Register post_read_request hook for logging at request reception.
- Register child_init hook for per-process state initialization.
- Initialize per-process server configuration structure.
- name: child_init
responsibilities:
- Initialize module state for each Apache child process.
- Reset per-process socket state (fd, timers, error flags).
- Attempt initial non-blocking connection to Unix socket if configured.
- name: post_read_request
responsibilities:
- Retrieve per-process server configuration (thread-safe).
- Ensure Unix socket is connected (with periodic reconnect).
- Build JSON log document for the request.
- Write JSON line to Unix socket using non-blocking I/O.
- Handle errors by dropping the current log line and rate-limiting
error reports into Apache error_log.
thread_safety:
model: per-process-state
description: >
Each Apache child process maintains its own socket state stored in the
server configuration structure (reqin_log_server_conf_t). This avoids
race conditions in worker and event MPMs where multiple threads share
a process.
implementation:
- State stored via ap_get_module_config(s->module_config)
- No global variables for socket state
- Each process has independent: socket_fd, connect timers, error timers
data_model:
json_line:
description: >
One JSON object per HTTP request, serialized on a single line and
terminated by "\n". Uses flat structure with header fields at root level.
structure: flat
fields:
- name: time
type: string
format: iso8601-with-timezone
example: "2026-02-26T11:59:30Z"
- name: timestamp
type: integer
unit: microseconds (expressed as nanoseconds)
description: >
Wall-clock timestamp in microseconds since Unix epoch, expressed
as nanoseconds for compatibility (multiplied by 1000).
Uses r->request_time (set by Apache at request reception).
The nanosecond representation is for API compatibility only.
example: 1708948770000000000
- name: scheme
type: string
description: Connection scheme evaluated via ap_http_scheme(r).
example: "https"
- name: src_ip
type: string
example: "192.0.2.10"
- name: src_port
type: integer
example: 45678
- name: dst_ip
type: string
example: "198.51.100.5"
- name: dst_port
type: integer
example: 443
- name: method
type: string
example: "GET"
- name: path
type: string
description: Cleaned and normalized path (r->parsed_uri.path).
example: "/api/users"
- name: query
type: string
description: >
Query string component from the parsed URI (r->parsed_uri.query).
Does not include the leading '?'. Allows detection of payloads like
SQLi or XSS passed in GET requests.
example: "id=1%20UNION%20SELECT"
- name: host
type: string
example: "example.com"
- name: http_version
type: string
example: "HTTP/1.1"
- name: keepalives
type: integer
description: >
Number of requests served over the current connection (r->connection->keepalives).
If 0, it indicates a newly established TCP connection.
If > 0, it confirms an active Keep-Alive session.
example: 2
- name: client_headers
type: array of strings
description: >
Ordered list of all HTTP header names as received from the client
(r->headers_in), preserving original order and case.
Useful for browser/bot fingerprinting (header order is client-specific).
example: ["Host", "User-Agent", "Accept", "Accept-Language", "Accept-Encoding"]
- name: content_length
type: integer
description: >
Declared size of the request body (POST payload),
extracted directly from the 'Content-Length' header.
example: 1048576
- name: header_<HeaderName>
type: string
description: >
Flattened header fields at root level. For each configured header <H>,
a field 'header_<H>' is added directly to the JSON root object.
Headers are only included if present in the request.
key_pattern: "header_<configured_header_name>"
optional: true
example:
header_X-Request-Id: "abcd-1234"
header_User-Agent: "curl/7.70.0"
example_full: |
{"time":"2026-02-26T11:59:30Z","timestamp":1708948770000000000,"scheme":"https","src_ip":"192.0.2.10","src_port":45678,"dst_ip":"198.51.100.5","dst_port":443,"method":"GET","path":"/api/users","query":"id=1","host":"example.com","http_version":"HTTP/1.1","keepalives":0,"client_headers":["Host","User-Agent","Accept","Accept-Language","Accept-Encoding","X-Request-Id"],"content_length":0,"header_X-Request-Id":"abcd-1234","header_User-Agent":"curl/7.70.0"}
configuration:
scope: global
directives:
- name: JsonSockLogEnabled
type: flag
context: server-config
default: "Off"
description: >
Enable or disable mod_reqin_log logging globally. Logging only occurs
when this directive is On and JsonSockLogSocket is set.
- name: JsonSockLogSocket
type: string
context: server-config
required_when_enabled: true
example: "/var/run/logcorrelator/http.socket"
description: >
Filesystem path of the Unix domain socket to which JSON log lines
will be written.
- name: JsonSockLogHeaders
type: list
context: server-config
value_example: ["X-Request-Id", "X-Trace-Id", "User-Agent", "Referer",
"X-Forwarded-For", "Sec-CH-UA", "Sec-CH-UA-Mobile", "Sec-CH-UA-Platform",
"Sec-Fetch-Dest", "Sec-Fetch-Mode", "Sec-Fetch-Site",
"Accept", "Accept-Language", "Accept-Encoding"]
description: >
List of HTTP header names to log. For each configured header <H>,
the module adds a JSON field 'header_<H>' at the root level of the
JSON log entry (flat structure). Order matters for applying the
JsonSockLogMaxHeaders limit.
- name: JsonSockLogMaxHeaders
type: integer
context: server-config
default: 25
min: 0
description: >
Maximum number of headers from JsonSockLogHeaders to actually log.
If more headers are configured, only the first N are considered.
- name: JsonSockLogMaxHeaderValueLen
type: integer
context: server-config
default: 256
min: 1
description: >
Maximum length in characters for each logged header value.
Values longer than this limit are truncated before JSON encoding.
- name: JsonSockLogReconnectInterval
type: integer
context: server-config
default: 10
unit: seconds
description: >
Minimal delay between two connection attempts to the Unix socket after
a failure. Used to avoid reconnect attempts on every request.
- name: JsonSockLogErrorReportInterval
type: integer
context: server-config
default: 10
unit: seconds
description: >
Minimal delay between two error messages emitted into Apache error_log
for repeated I/O or connection errors on the Unix socket.
behavior:
enabling_rules:
- JsonSockLogEnabled must be On.
- JsonSockLogSocket must be set to a non-empty path.
header_handling:
- Built-in blacklist prevents logging of sensitive headers by default.
- Blacklisted headers: Authorization, Cookie, Set-Cookie, X-Api-Key,
X-Auth-Token, Proxy-Authorization, WWW-Authenticate.
- Blacklisted headers are silently skipped (logged at DEBUG level only).
- If a configured header is absent in a request, the corresponding
JSON key is omitted from the log entry.
- Header values are truncated to JsonSockLogMaxHeaderValueLen characters.
io:
socket:
type: unix-domain
protocol: SOCK_DGRAM
mode: client
path_source: JsonSockLogSocket
connection:
persistence: false
non_blocking: true
lifecycle:
open:
- Create DGRAM socket and set default destination address via connect()
during child_init if enabled.
- Re-attempt addressing after reconnect interval expiry if target
was previously unavailable.
failure:
- On missing target socket (ECONNREFUSED/ENOENT), mark target as unavailable.
- Do not block the worker process.
reconnect:
strategy: time-based
interval_seconds: config.JsonSockLogReconnectInterval
trigger: >
When a request arrives and the last target resolution attempt time is older
than reconnect interval, a new attempt to address the socket is made.
write:
format: json_object
mode: non-blocking
atomicity: >
Full JSON line is sent as a single datagram. Message size must not exceed
system DGRAM limits or MAX_JSON_SIZE (64KB).
error_handling:
on_eagain_or_ewouldblock:
action: drop-current-log-line
note: "OS buffer full (receiver is too slow). Do not retry, do not spam error_log."
on_econnrefused_or_enoent:
action:
- close_socket
- mark_target_unavailable
- schedule_reconnect
note: "Target socket closed or deleted by log receiver."
generic_errors:
action: drop-current-log-line
drop_policy:
description: >
Logging errors never impact client response. The current log line is
silently dropped except for throttled error_log reporting.
error_handling:
apache_error_log_reporting:
enabled: true
throttle_interval_seconds: config.JsonSockLogErrorReportInterval
events:
- type: connect_failure
message_template: "mod_reqin_log: Unix socket connect failed: [errno_detail]"
- type: write_failure
message_template: "mod_reqin_log: Unix socket write failed: [errno_detail]"
fatal_conditions:
- description: >
Misconfiguration (JsonSockLogEnabled On but missing JsonSockLogSocket)
should be reported at startup as a configuration error.
- description: >
Any internal JSON-encoding failure should be treated as non-fatal
(drop current log and optionally emit a throttled error_log entry).
constraints:
performance:
objectives:
- Logging overhead per request should be minimal and non-blocking.
- No dynamic allocations in hot path beyond what is strictly necessary
(prefer APR pools where possible).
design_choices:
- Single JSON serialization pass per request.
- Use non-blocking I/O to avoid stalling worker threads/processes.
- Avoid reconnect attempts on every request via time-based backoff.
security:
notes:
- Module includes built-in blacklist of sensitive headers to prevent
accidental credential leakage (Authorization, Cookie, X-Api-Key, etc.).
- Socket permissions default to 0o660 (owner/group only) for security.
- Recommended socket path: /var/run/logcorrelator/http.socket (not /tmp).
- Use environment variable MOD_REQINLOG_SOCKET to configure socket path.
- Module does not anonymize IPs (data protection is delegated to configuration).
- No requests are rejected due to logging failures.
hardening:
- Socket path length validated against system limit (108 bytes).
- JSON log line size limited to 64KB to prevent memory exhaustion DoS.
- NULL pointer checks on all connection/request fields.
- Thread-safe socket FD access via mutex (worker/event MPMs).
- Error logging reduced to prevent information disclosure.
robustness:
requirements:
- Logging failures must not crash Apache worker processes.
- Module must behave correctly under high traffic, socket disappearance,
and repeated connect failures.
testing_strategy:
unit_tests:
framework: cmocka
location: tests/unit/test_module_real.c
focus:
- JSON serialization with header truncation and header count limits.
- Dynamic buffer operations (dynbuf_t) with resize handling.
- ISO8601 timestamp formatting.
- Header value truncation to JsonSockLogMaxHeaderValueLen.
- Control character escaping in JSON strings.
execution:
- docker build -f Dockerfile.tests .
- docker run --rm <image> ctest --output-on-failure
ci_strategy:
description: >
All builds, tests and packaging are executed inside Docker containers
using GitLab CI with Docker-in-Docker (dind). No RPM build or test is
allowed on bare-metal or shared CI runners.
tools:
orchestrator: GitLab CI
container_engine: docker
dind: true
workflow_file: .gitlab-ci.yml
constraints:
no_host_builds: true
description: >
It is forbidden to run rpmbuild, unit tests or package verification
directly on the CI host. All steps MUST run inside Docker containers
defined by project Dockerfiles.
rpm_strategy: >
Separate RPMs are built for each major RHEL/CentOS/Rocky/AlmaLinux version
(el8, el9, el10) due to glibc and httpd-devel incompatibilities across
major versions. A single RPM cannot work across all versions.
RPM packages are built using rpmbuild with mod_reqin_log.spec file.
rpm_changelog:
policy: mandatory
description: >
For every version or release bump of the RPM (Version or Release tag
in mod_reqin_log.spec), the %changelog section MUST be updated with:
- date, packager, new version-release
- brief description of the changes.
validation:
- A CI job MUST fail if Version/Release changed and no new %changelog
entry is present.
- Changelog is the single source of truth for packaged changes.
stages:
- name: validate-spec
description: >
Ensure that any change to Version/Release in mod_reqin_log.spec
is accompanied by a new %changelog entry.
containerized: true
dockerfile: Dockerfile.tools
checks:
- script: scripts/check_spec_changelog.sh mod_reqin_log.spec
fail_on_missing_changelog: true
- name: build
description: >
Build all RPM packages (el8, el9, el10) using Dockerfile.package
with multi-stage build, entirely inside a Docker container.
dockerfile: Dockerfile.package
containerized: true
artifacts:
- dist/rpm/*.el8.*.rpm
- dist/rpm/*.el9.*.rpm
- dist/rpm/*.el10.*.rpm
- name: test
description: >
Run unit tests (C with cmocka) inside Docker containers, using
Dockerfile.tests as the only execution environment.
dockerfile: Dockerfile.tests
containerized: true
execution: ctest --output-on-failure
- name: verify
description: >
Verify RPM installation and module loading on each target distribution
by running containers for each OS.
containerized: true
jobs:
- name: verify-rpm-el8
image: rockylinux:8
steps:
- rpm -qi mod_reqin_log
- httpd -M | grep reqin_log
- name: verify-rpm-el9
image: rockylinux:9
steps:
- rpm -qi mod_reqin_log
- httpd -M | grep reqin_log
- name: verify-rpm-el10
image: almalinux:10
steps:
- rpm -qi mod_reqin_log
- httpd -M | grep reqin_log

View File

@ -0,0 +1,40 @@
# mod_reqin_log example configuration
# Load this configuration in your Apache httpd.conf or a separate included file
# Load the module (adjust path as needed)
LoadModule reqin_log_module modules/mod_reqin_log.so
# Enable mod_reqin_log
JsonSockLogEnabled On
# Unix domain socket path for JSON log output
# Important: if JsonSockLogEnabled is On and this directive is missing/empty,
# Apache startup fails due to strict configuration validation.
JsonSockLogSocket "/var/run/logcorrelator/http.socket"
# HTTP headers to include in the JSON log
# Warning: Be careful not to log sensitive headers like Authorization, Cookie, etc.
JsonSockLogHeaders X-Request-Id X-Trace-Id User-Agent Referer X-Forwarded-For \
Sec-CH-UA Sec-CH-UA-Mobile Sec-CH-UA-Platform \
Sec-Fetch-Dest Sec-Fetch-Mode Sec-Fetch-Site \
Accept Accept-Language Accept-Encoding Content-Type
# Maximum number of headers to log (from the configured list)
JsonSockLogMaxHeaders 25
# Maximum length of each header value (longer values are truncated)
JsonSockLogMaxHeaderValueLen 256
# Minimum delay between reconnect attempts to the Unix socket (seconds)
JsonSockLogReconnectInterval 10
# Minimum delay between error messages to Apache error_log (seconds)
JsonSockLogErrorReportInterval 10
# Log level for module messages: DEBUG, INFO, WARNING, ERROR, EMERG (default: WARNING)
# DEBUG: Log all messages including header skipping and buffer truncation
# INFO: Log informational messages
# WARNING: Log warnings (default)
# ERROR: Log only errors
# EMERG: Log only emergency messages
JsonSockLogLevel WARNING

View File

@ -0,0 +1,123 @@
%global spec_version 1.0.19
Name: mod_reqin_log
Version: %{spec_version}
Release: 1%{?dist}
Summary: Apache HTTPD module for logging HTTP requests as JSON to Unix socket
License: Apache-2.0
URL: https://github.com/example/mod_reqin_log
Vendor: Developer <dev@example.com>
BuildArch: x86_64
Requires: httpd
%description
Apache HTTPD module for logging HTTP requests as JSON to Unix socket.
Features non-blocking I/O with automatic reconnection, configurable headers
with truncation support, and built-in sensitive headers blacklist.
%prep
# No source extraction needed - binaries are pre-built
%build
# No build needed - binaries are pre-built
%install
mkdir -p %{buildroot}/%{_libdir}/httpd/modules
mkdir -p %{buildroot}/%{_sysconfdir}/httpd/conf.d
mkdir -p %{buildroot}/%{_docdir}/%{name}
install -m 755 %{_pkgroot}/%{_libdir}/httpd/modules/mod_reqin_log.so %{buildroot}/%{_libdir}/httpd/modules/
install -m 644 %{_pkgroot}/%{_sysconfdir}/httpd/conf.d/mod_reqin_log.conf %{buildroot}/%{_sysconfdir}/httpd/conf.d/
%files
%{_libdir}/httpd/modules/mod_reqin_log.so
%config(noreplace) %{_sysconfdir}/httpd/conf.d/mod_reqin_log.conf
%doc %{_docdir}/%{name}
%changelog
* Thu Mar 05 2026 Developer <dev@example.com> - 1.0.19
- FEATURE: Add client_headers JSON field - ordered list of all header names
as received from the client, preserving original order and case
- DOC: Update architecture.yml with client_headers field and example_full
* Thu Mar 05 2026 Developer <dev@example.com> - 1.0.18
- FIX: JsonSockLogMaxHeaders now counts configured headers (by position in list)
regardless of their presence in the request, matching the documented behavior
* Thu Mar 05 2026 Developer <dev@example.com> - 1.0.17
- CONFIG: Extend default JsonSockLogHeaders list (User-Agent, Referer, X-Forwarded-For,
Sec-CH-UA*, Sec-Fetch-*, Accept, Accept-Language, Accept-Encoding)
- CONFIG: Raise DEFAULT_MAX_HEADERS from 10 to 25
- DOC: Update architecture.yml and conf/mod_reqin_log.conf accordingly
* Thu Mar 05 2026 Developer <dev@example.com> - 1.0.16
- FIX: Skip subrequests and internal redirects to log only the original client request
- DOC: Document subrequest/redirect filtering in architecture.yml
* Thu Mar 05 2026 Developer <dev@example.com> - 1.0.15
- FIX: timestamp field now uses r->request_time (request reception time) instead of apr_time_now()
- DOC: Remove unparsed_uri and fragment fields from architecture.yml (not logged)
- DOC: Update timestamp description and example_full in architecture.yml
* Mon Mar 02 2026 Developer <dev@example.com> - 1.0.14
- REFACTOR: Harmonize JSON field construction - all fields now end with comma
- FIX: Remove duplicate comma between query and host fields
- FIX: Fix buffer corruption in dynbuf_append (copy null terminator)
- PACKAGING: Config file marked as %config(noreplace)
- CHANGE: Remove unparsed_uri, fragment, content_length fields
* Mon Mar 02 2026 Developer <dev@example.com> - 1.0.13
- FIX: Correct JSON string length parameters for query and fragment fields
- FIX: Add null-termination after buffer reallocation in dynbuf_append
- CHANGE: Remove unparsed_uri, fragment, and content_length fields from JSON output
- TEST: Update unit tests to match dynbuf_append fix
* Mon Mar 02 2026 Developer <dev@example.com> - 1.0.9
- CHANGE: Remove req_id field from JSON output
- FEATURE: Add query and fragment fields (URI components)
* Mon Mar 02 2026 Developer <dev@example.com> - 1.0.8
- FEATURE: Add req_id, scheme, unparsed_uri, args, keepalives, content_length fields to JSON output
- FIX: Change socket type from SOCK_STREAM to SOCK_DGRAM per architecture.yml
* Sun Mar 01 2026 Developer <dev@example.com> - 1.0.6
- BUILD: Fix RPM package paths in Dockerfile.package (el8, el9, el10 directories)
- BUILD: Fix Makefile RPM extraction with separate volume mounts
- BUILD: Remove unused scripts (build.sh, test.sh)
- BUILD: Remove Python integration tests (not automated in CI)
- DOCS: Update README.md and architecture.yml for RPM-only packaging
- CLEANUP: Remove DEB and el7 references
* Sat Feb 28 2026 Developer <dev@example.com> - 1.0.2
- SECURITY: Add input sanitization for method, path, host, and http_version fields
- SECURITY: Add Host header truncation (256 chars max) to prevent log injection
- IMPROVEMENT: Add LOG_THROTTLED macro for consistent error reporting
- IMPROVEMENT: Improve socket state double-check pattern
- IMPROVEMENT: Fix const qualifier warnings in get_header() function
- IMPROVEMENT: Add flags field to module definition
- IMPROVEMENT: Add -Wno-error=format-security to Makefile
- TEST: Add 4 new unit tests for input sanitization
- DOC: Clarify timestamp precision
- DOC: Update README and architecture.yml
- BUILD: Update package version to 1.0.2
* Fri Feb 27 2026 Developer <dev@example.com> - 1.0.1
- FIX: Fix socket reconnection logic
- FIX: Improve error logging to prevent error_log flooding
- IMPROVEMENT: Add built-in sensitive headers blacklist
- IMPROVEMENT: Add thread-safe socket FD access via mutex
- TEST: Add comprehensive unit tests
- TEST: Add integration tests for socket loss and recovery
- DOC: Add comprehensive README with configuration examples
- DOC: Add architecture.yml documenting module design decisions
* Thu Feb 26 2026 Developer <dev@example.com> - 1.0.0
- Initial release
- Apache HTTPD 2.4 module for logging HTTP requests as JSON to Unix socket
- Non-blocking I/O with automatic reconnection
- Configurable headers with truncation support
- Compatible with prefork, worker, and event MPMs
- Built-in sensitive headers blacklist
- Throttled error reporting to prevent log flooding

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,56 @@
/*
* mod_reqin_log.h - Apache HTTPD module for logging HTTP requests as JSON to Unix socket
*
* Copyright (c) 2026. All rights reserved.
*/
#ifndef MOD_REQIN_LOG_H
#define MOD_REQIN_LOG_H
#include "httpd.h"
#include "http_config.h"
#include "apr_tables.h"
/* Module name */
#define MOD_REQIN_LOG_NAME "mod_reqin_log"
/* Default configuration values */
#define DEFAULT_MAX_HEADERS 25
#define DEFAULT_MAX_HEADER_VALUE_LEN 256
#define DEFAULT_RECONNECT_INTERVAL 10
#define DEFAULT_ERROR_REPORT_INTERVAL 10
/* Module configuration structure */
typedef struct {
int enabled;
const char *socket_path;
apr_array_header_t *headers;
int max_headers;
int max_header_value_len;
int reconnect_interval;
int error_report_interval;
} reqin_log_config_t;
/* External module declaration */
extern module AP_MODULE_DECLARE_DATA reqin_log_module;
/* ====== Fingerprinting HTTP/2 passif ====== */
/* Clés des notes de connexion stockant le fingerprint HTTP/2 parsé */
#define H2_NOTE_FINGERPRINT "reqin_h2_fp" /* Fingerprint Akamai complet */
#define H2_NOTE_SETTINGS "reqin_h2_set" /* Entrées SETTINGS brutes */
#define H2_NOTE_WUPDATE "reqin_h2_wu" /* Incrément WINDOW_UPDATE */
#define H2_NOTE_PSEUDO_ORDER "reqin_h2_ps" /* Ordre pseudo-headers */
#define H2_NOTE_HAS_PRIORITY "reqin_h2_pri" /* Flag PRIORITY présent */
#define H2_NOTE_PARSED "reqin_h2_done" /* Marqueur "déjà parsé" */
/* Clés des notes pour chaque paramètre SETTINGS individuel (RFC 9113 §6.5.2) */
#define H2_NOTE_SET_HEADER_TABLE_SIZE "reqin_h2_s1" /* ID 1 */
#define H2_NOTE_SET_ENABLE_PUSH "reqin_h2_s2" /* ID 2 */
#define H2_NOTE_SET_MAX_CONCURRENT_STREAMS "reqin_h2_s3" /* ID 3 */
#define H2_NOTE_SET_INITIAL_WINDOW_SIZE "reqin_h2_s4" /* ID 4 */
#define H2_NOTE_SET_MAX_FRAME_SIZE "reqin_h2_s5" /* ID 5 */
#define H2_NOTE_SET_MAX_HEADER_LIST_SIZE "reqin_h2_s6" /* ID 6 */
#define H2_NOTE_SET_ENABLE_CONNECT "reqin_h2_s8" /* ID 8 */
#endif /* MOD_REQIN_LOG_H */

View File

@ -0,0 +1,333 @@
/*
* test_config_parsing.c - Unit tests for configuration parsing
*/
#include <stdarg.h>
#include <stddef.h>
#include <setjmp.h>
#include <cmocka.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <limits.h>
/* Default configuration values */
#define DEFAULT_MAX_HEADERS 10
#define DEFAULT_MAX_HEADER_VALUE_LEN 256
#define DEFAULT_RECONNECT_INTERVAL 10
#define DEFAULT_ERROR_REPORT_INTERVAL 10
#define MAX_SOCKET_PATH_LEN 108
/* Mock configuration structure */
typedef struct {
int enabled;
const char *socket_path;
int max_headers;
int max_header_value_len;
int reconnect_interval;
int error_report_interval;
} mock_config_t;
/* Mock parsing functions */
static int parse_enabled(const char *value)
{
if (strcasecmp(value, "on") == 0 || strcmp(value, "1") == 0) {
return 1;
}
return 0;
}
static const char *parse_socket_path(const char *value)
{
if (value == NULL || strlen(value) == 0) {
return NULL;
}
if (strlen(value) >= MAX_SOCKET_PATH_LEN) {
return NULL;
}
return value;
}
static int parse_int_strict(const char *value, int *result)
{
char *endptr = NULL;
long val;
if (value == NULL || *value == '\0' || result == NULL) {
return -1;
}
errno = 0;
val = strtol(value, &endptr, 10);
if (errno != 0 || endptr == value || *endptr != '\0' || val < INT_MIN || val > INT_MAX) {
return -1;
}
*result = (int)val;
return 0;
}
static int parse_max_headers(const char *value, int *result)
{
if (parse_int_strict(value, result) != 0 || *result < 0) {
return -1;
}
return 0;
}
static int parse_interval(const char *value, int *result)
{
if (parse_int_strict(value, result) != 0 || *result < 0) {
return -1;
}
return 0;
}
static int parse_max_header_value_len(const char *value, int *result)
{
if (parse_int_strict(value, result) != 0 || *result < 1) {
return -1;
}
return 0;
}
/* Test: Parse enabled On */
static void test_parse_enabled_on(void **state)
{
(void)state;
assert_int_equal(parse_enabled("On"), 1);
assert_int_equal(parse_enabled("on"), 1);
assert_int_equal(parse_enabled("ON"), 1);
assert_int_equal(parse_enabled("1"), 1);
}
/* Test: Parse enabled Off */
static void test_parse_enabled_off(void **state)
{
(void)state;
assert_int_equal(parse_enabled("Off"), 0);
assert_int_equal(parse_enabled("off"), 0);
assert_int_equal(parse_enabled("OFF"), 0);
assert_int_equal(parse_enabled("0"), 0);
}
/* Test: Parse socket path valid */
static void test_parse_socket_path_valid(void **state)
{
(void)state;
const char *result = parse_socket_path("/var/run/logcorrelator/http.socket");
assert_string_equal(result, "/var/run/logcorrelator/http.socket");
}
/* Test: Parse socket path empty */
static void test_parse_socket_path_empty(void **state)
{
(void)state;
const char *result = parse_socket_path("");
assert_null(result);
}
/* Test: Parse socket path NULL */
static void test_parse_socket_path_null(void **state)
{
(void)state;
const char *result = parse_socket_path(NULL);
assert_null(result);
}
/* Test: Parse socket path max length valid */
static void test_parse_socket_path_max_len_valid(void **state)
{
(void)state;
char path[MAX_SOCKET_PATH_LEN];
memset(path, 'a', MAX_SOCKET_PATH_LEN - 1);
path[MAX_SOCKET_PATH_LEN - 1] = '\0';
assert_non_null(parse_socket_path(path));
}
/* Test: Parse socket path max length invalid */
static void test_parse_socket_path_max_len_invalid(void **state)
{
(void)state;
char path[MAX_SOCKET_PATH_LEN + 1];
memset(path, 'b', MAX_SOCKET_PATH_LEN);
path[MAX_SOCKET_PATH_LEN] = '\0';
assert_null(parse_socket_path(path));
}
/* Test: Parse max headers valid */
static void test_parse_max_headers_valid(void **state)
{
int result;
(void)state;
assert_int_equal(parse_max_headers("10", &result), 0);
assert_int_equal(result, 10);
assert_int_equal(parse_max_headers("0", &result), 0);
assert_int_equal(result, 0);
assert_int_equal(parse_max_headers("100", &result), 0);
assert_int_equal(result, 100);
}
/* Test: Parse max headers invalid */
static void test_parse_max_headers_invalid(void **state)
{
int result;
(void)state;
assert_int_equal(parse_max_headers("-1", &result), -1);
assert_int_equal(parse_max_headers("abc", &result), -1);
assert_int_equal(parse_max_headers("10abc", &result), -1);
}
/* Test: Parse reconnect interval valid */
static void test_parse_reconnect_interval_valid(void **state)
{
int result;
(void)state;
assert_int_equal(parse_interval("10", &result), 0);
assert_int_equal(result, 10);
assert_int_equal(parse_interval("0", &result), 0);
assert_int_equal(result, 0);
assert_int_equal(parse_interval("60", &result), 0);
assert_int_equal(result, 60);
}
/* Test: Parse reconnect interval invalid */
static void test_parse_reconnect_interval_invalid(void **state)
{
int result;
(void)state;
assert_int_equal(parse_interval("-5", &result), -1);
assert_int_equal(parse_interval("abc", &result), -1);
assert_int_equal(parse_interval("10abc", &result), -1);
}
/* Test: Parse max header value length valid */
static void test_parse_max_header_value_len_valid(void **state)
{
int result;
(void)state;
assert_int_equal(parse_max_header_value_len("1", &result), 0);
assert_int_equal(result, 1);
assert_int_equal(parse_max_header_value_len("256", &result), 0);
assert_int_equal(result, 256);
}
/* Test: Parse max header value length invalid */
static void test_parse_max_header_value_len_invalid(void **state)
{
int result;
(void)state;
assert_int_equal(parse_max_header_value_len("0", &result), -1);
assert_int_equal(parse_max_header_value_len("-1", &result), -1);
assert_int_equal(parse_max_header_value_len("10abc", &result), -1);
}
/* Test: strict numeric parsing invalid suffix for all int directives */
static void test_strict_numeric_invalid_suffix_all(void **state)
{
int result;
(void)state;
assert_int_equal(parse_max_headers("10abc", &result), -1);
assert_int_equal(parse_interval("10abc", &result), -1);
assert_int_equal(parse_max_header_value_len("10abc", &result), -1);
}
/* Test: Default configuration values */
static void test_default_config_values(void **state)
{
(void)state;
assert_int_equal(DEFAULT_MAX_HEADERS, 10);
assert_int_equal(DEFAULT_MAX_HEADER_VALUE_LEN, 256);
assert_int_equal(DEFAULT_RECONNECT_INTERVAL, 10);
assert_int_equal(DEFAULT_ERROR_REPORT_INTERVAL, 10);
}
/* Test: Configuration validation - enabled requires socket */
static void test_config_validation_enabled_requires_socket(void **state)
{
int enabled = 1;
const char *socket = "/var/run/socket";
(void)state;
assert_true(enabled == 0 || socket != NULL);
socket = NULL;
assert_false(enabled == 0 || socket != NULL);
}
/* Test: Configuration validation - enabled with empty socket is invalid */
static void test_config_validation_enabled_with_empty_socket(void **state)
{
int enabled = 1;
const char *socket = parse_socket_path("");
(void)state;
assert_false(enabled == 0 || socket != NULL);
}
/* Test: Header value length validation */
static void test_header_value_len_validation(void **state)
{
int result;
(void)state;
assert_int_equal(parse_max_header_value_len("1", &result), 0);
assert_true(result >= 1);
assert_int_equal(parse_max_header_value_len("0", &result), -1);
}
/* Test: Large but valid values */
static void test_large_valid_values(void **state)
{
int result;
(void)state;
assert_int_equal(parse_max_headers("1000000", &result), 0);
assert_int_equal(result, 1000000);
assert_int_equal(parse_interval("86400", &result), 0);
assert_int_equal(result, 86400);
}
int main(void)
{
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_parse_enabled_on),
cmocka_unit_test(test_parse_enabled_off),
cmocka_unit_test(test_parse_socket_path_valid),
cmocka_unit_test(test_parse_socket_path_empty),
cmocka_unit_test(test_parse_socket_path_null),
cmocka_unit_test(test_parse_socket_path_max_len_valid),
cmocka_unit_test(test_parse_socket_path_max_len_invalid),
cmocka_unit_test(test_parse_max_headers_valid),
cmocka_unit_test(test_parse_max_headers_invalid),
cmocka_unit_test(test_parse_reconnect_interval_valid),
cmocka_unit_test(test_parse_reconnect_interval_invalid),
cmocka_unit_test(test_parse_max_header_value_len_valid),
cmocka_unit_test(test_parse_max_header_value_len_invalid),
cmocka_unit_test(test_strict_numeric_invalid_suffix_all),
cmocka_unit_test(test_default_config_values),
cmocka_unit_test(test_config_validation_enabled_requires_socket),
cmocka_unit_test(test_config_validation_enabled_with_empty_socket),
cmocka_unit_test(test_header_value_len_validation),
cmocka_unit_test(test_large_valid_values),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}

View File

@ -0,0 +1,458 @@
/*
* test_h2_parsing.c — Tests unitaires du fingerprinting HTTP/2 passif.
*
* Les fonctions testées (hpack_int_decode, h2_extract_pseudo_order,
* h2_parse_preface_buf) sont réimplimentées localement pour éviter les
* dépendances Apache/APR. La logique est identique à mod_reqin_log.c.
*/
#include <stdarg.h>
#include <stddef.h>
#include <setjmp.h>
#include <cmocka.h>
#include <string.h>
#include <stdio.h>
#include <stdint.h>
/* ====== Réimplémentation locale des fonctions H2 ====== */
static int hpack_int_decode(const unsigned char *buf, size_t len, int prefix,
size_t *pos, unsigned int *out)
{
unsigned int mask = (1u << prefix) - 1u;
unsigned int b, m;
if (*pos >= len) return 0;
*out = buf[(*pos)++] & mask;
if (*out < mask) return 1;
m = 0;
while (*pos < len) {
b = buf[(*pos)++];
*out += (b & 0x7fu) << m;
m += 7;
if (!(b & 0x80u)) return 1;
if (m > 28) return 0;
}
return 0;
}
static char h2_hpack_pseudo(unsigned int index)
{
switch (index) {
case 1: return 'a';
case 2: case 3: return 'm';
case 4: case 5: return 'p';
case 6: case 7: return 's';
default: return 0;
}
}
static void h2_extract_pseudo_order(const unsigned char *hpack, size_t len, char *out)
{
size_t pos = 0;
int out_pos = 0;
int first = 1;
while (pos < len && out_pos < 7) {
unsigned char byte = hpack[pos];
if (byte & 0x80u) {
unsigned int idx = 0;
if (!hpack_int_decode(hpack, len, 7, &pos, &idx)) break;
if (idx == 0) break;
char c = h2_hpack_pseudo(idx);
if (!c) break;
if (!first) out[out_pos++] = ',';
out[out_pos++] = c;
first = 0;
} else if ((byte & 0xe0u) == 0x20u) {
unsigned int sz = 0;
if (!hpack_int_decode(hpack, len, 5, &pos, &sz)) break;
} else {
break;
}
}
out[out_pos] = '\0';
}
/* Résultat de h2_parse_preface_buf — version allégée (pas d'APR) */
typedef struct {
char settings[256];
char wupdate[16];
char pseudo[16];
char fingerprint[512];
int has_priority;
int is_h2;
} h2_result_t;
static void h2_parse_preface_buf(const char *buf, size_t len, h2_result_t *res)
{
static const char H2_MAGIC[] = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n";
const size_t MAGIC_LEN = 24u;
const size_t FRAME_HDR = 9u;
memset(res, 0, sizeof(*res));
strcpy(res->wupdate, "0");
if (len < MAGIC_LEN || memcmp(buf, H2_MAGIC, MAGIC_LEN) != 0) return;
int settings_out = 0;
size_t pos = MAGIC_LEN;
while (pos + FRAME_HDR <= len) {
size_t frame_len = ((unsigned char)buf[pos] << 16)
| ((unsigned char)buf[pos+1] << 8)
| (unsigned char)buf[pos+2];
unsigned char type = (unsigned char)buf[pos+3];
unsigned char flags = (unsigned char)buf[pos+4];
uint32_t stream_id = (((unsigned char)buf[pos+5] & 0x7fu) << 24)
| ((unsigned char)buf[pos+6] << 16)
| ((unsigned char)buf[pos+7] << 8)
| (unsigned char)buf[pos+8];
pos += FRAME_HDR;
if (pos + frame_len > len) break;
if (type == 0x04u && stream_id == 0 && !(flags & 0x01u)) {
size_t sp = 0;
while (sp + 6 <= frame_len &&
settings_out < (int)sizeof(res->settings) - 24) {
uint16_t id = ((unsigned char)buf[pos + sp] << 8)
| (unsigned char)buf[pos + sp + 1];
uint32_t val = ((unsigned char)buf[pos + sp + 2] << 24)
| ((unsigned char)buf[pos + sp + 3] << 16)
| ((unsigned char)buf[pos + sp + 4] << 8)
| (unsigned char)buf[pos + sp + 5];
sp += 6;
if (settings_out > 0)
res->settings[settings_out++] = ',';
settings_out += snprintf(res->settings + settings_out,
(int)sizeof(res->settings) - settings_out,
"%u:%u", id, val);
}
} else if (type == 0x08u && stream_id == 0) {
if (frame_len >= 4) {
uint32_t inc = (((unsigned char)buf[pos] & 0x7fu) << 24)
| ((unsigned char)buf[pos+1] << 16)
| ((unsigned char)buf[pos+2] << 8)
| (unsigned char)buf[pos+3];
snprintf(res->wupdate, sizeof(res->wupdate), "%u", inc);
}
} else if (type == 0x01u && stream_id > 0) {
size_t hpack_start = 0;
int parse_ok = 1;
if ((flags & 0x08u) && parse_ok) {
if (hpack_start >= frame_len) {
parse_ok = 0;
} else {
unsigned char pad_len = (unsigned char)buf[pos + hpack_start++];
if (frame_len < hpack_start + (size_t)pad_len)
parse_ok = 0;
else
frame_len -= (size_t)pad_len;
}
}
if ((flags & 0x20u) && parse_ok) {
if (hpack_start + 5u > frame_len) {
parse_ok = 0;
} else {
hpack_start += 5u;
res->has_priority = 1;
}
}
if (parse_ok && hpack_start < frame_len) {
h2_extract_pseudo_order(
(const unsigned char *)(buf + pos + hpack_start),
frame_len - hpack_start,
res->pseudo
);
}
pos += frame_len;
break;
}
pos += frame_len;
}
if (res->settings[0] != '\0') {
res->is_h2 = 1;
snprintf(res->fingerprint, sizeof(res->fingerprint), "%s|%s|%d|%s",
res->settings, res->wupdate, res->has_priority, res->pseudo);
}
}
/* ====== Données de test : preface Chrome 120 ====== */
/*
* Preface HTTP/2 Chrome 120 (capturée) :
* Magic (24 octets)
* SETTINGS frame : HEADER_TABLE_SIZE=65536, ENABLE_PUSH=0,
* INITIAL_WINDOW_SIZE=6291456, MAX_HEADER_LIST_SIZE=262144
* WINDOW_UPDATE : incrément 15663105
* HEADERS stream 1 : :method GET, :authority, :scheme https, :path /
* → ordre HPACK indexé : 0x82(GET), 0x81(:auth), 0x87(https), 0x84(/)
*/
static const unsigned char CHROME_PREFACE[] = {
/* Magic */
'P','R','I',' ','*',' ','H','T','T','P','/','2','.','0','\r','\n',
'\r','\n','S','M','\r','\n','\r','\n',
/* SETTINGS frame : length=24, type=0x04, flags=0x00, stream=0 */
0x00, 0x00, 0x18, /* length = 24 = 4×6 */
0x04, /* type SETTINGS */
0x00, /* flags = 0 */
0x00, 0x00, 0x00, 0x00, /* stream 0 */
/* Entry 1: HEADER_TABLE_SIZE (1) = 65536 = 0x00010000 */
0x00, 0x01, 0x00, 0x01, 0x00, 0x00,
/* Entry 2: ENABLE_PUSH (2) = 0 */
0x00, 0x02, 0x00, 0x00, 0x00, 0x00,
/* Entry 3: INITIAL_WINDOW_SIZE (4) = 6291456 = 0x00600000 */
0x00, 0x04, 0x00, 0x60, 0x00, 0x00,
/* Entry 4: MAX_HEADER_LIST_SIZE (6) = 262144 = 0x00040000 */
0x00, 0x06, 0x00, 0x04, 0x00, 0x00,
/* WINDOW_UPDATE frame : length=4, type=0x08, flags=0, stream=0 */
0x00, 0x00, 0x04,
0x08,
0x00,
0x00, 0x00, 0x00, 0x00,
/* increment = 15663105 = 0x00EF0001 */
0x00, 0xEF, 0x00, 0x01,
/* HEADERS frame : length=14, type=0x01, flags=0x05 (END_STREAM|END_HEADERS), stream=1 */
0x00, 0x00, 0x0E,
0x01,
0x05,
0x00, 0x00, 0x00, 0x01,
/* HPACK : :method GET (0x82), :authority (0x81), :scheme https (0x87), :path / (0x84) */
/* → ordre Chrome : m,a,s,p */
0x82, 0x81, 0x87, 0x84,
/* + quelques headers supplémentaires (indices statiques) */
0x86, /* :scheme http (index 6, régulier → stop après pseudo) */
0x53, /* accept (sans valeur — littéral, arrête le scan) */
0x00, 0x05, 0x74, 0x65, 0x78, 0x74, 0x2F, 0x68, 0x74, 0x6D, 0x6C
};
/* ====== Données de test : preface Firefox 120 ====== */
/*
* Preface HTTP/2 Firefox 120 :
* SETTINGS: HEADER_TABLE_SIZE=65536, INITIAL_WINDOW_SIZE=131072, MAX_FRAME_SIZE=16384
* WINDOW_UPDATE: 12517377
* HEADERS: :method GET (0x82), :path / (0x84), :scheme https (0x87), :authority (0x81)
* → ordre Firefox : m,p,s,a
*/
static const unsigned char FIREFOX_PREFACE[] = {
/* Magic */
'P','R','I',' ','*',' ','H','T','T','P','/','2','.','0','\r','\n',
'\r','\n','S','M','\r','\n','\r','\n',
/* SETTINGS frame : length=18, type=0x04, flags=0x00, stream=0 */
0x00, 0x00, 0x12,
0x04,
0x00,
0x00, 0x00, 0x00, 0x00,
/* HEADER_TABLE_SIZE (1) = 65536 */
0x00, 0x01, 0x00, 0x01, 0x00, 0x00,
/* INITIAL_WINDOW_SIZE (4) = 131072 = 0x00020000 */
0x00, 0x04, 0x00, 0x02, 0x00, 0x00,
/* MAX_FRAME_SIZE (5) = 16384 = 0x00004000 */
0x00, 0x05, 0x00, 0x00, 0x40, 0x00,
/* WINDOW_UPDATE : increment = 12517377 = 0x00BF0001 */
0x00, 0x00, 0x04,
0x08,
0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0xBF, 0x00, 0x01,
/* HEADERS frame : length=4, type=0x01, flags=0x05, stream=1 */
0x00, 0x00, 0x04,
0x01,
0x05,
0x00, 0x00, 0x00, 0x01,
/* HPACK : :method GET (0x82), :path / (0x84), :scheme https (0x87), :authority (0x81) */
/* → ordre Firefox : m,p,s,a */
0x82, 0x84, 0x87, 0x81
};
/* ====== Données de test : flux HTTP/1.1 (ne doit pas matcher) ====== */
static const char HTTP1_DATA[] =
"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n";
/* ====== Tests ====== */
static void test_chrome_settings_parsed(void **state)
{
(void)state;
h2_result_t res;
h2_parse_preface_buf((const char *)CHROME_PREFACE, sizeof(CHROME_PREFACE), &res);
assert_int_equal(res.is_h2, 1);
/* SETTINGS attendus : 1:65536,2:0,4:6291456,6:262144 */
assert_string_equal(res.settings, "1:65536,2:0,4:6291456,6:262144");
}
static void test_chrome_window_update(void **state)
{
(void)state;
h2_result_t res;
h2_parse_preface_buf((const char *)CHROME_PREFACE, sizeof(CHROME_PREFACE), &res);
assert_string_equal(res.wupdate, "15663105");
}
static void test_chrome_pseudo_order(void **state)
{
(void)state;
h2_result_t res;
h2_parse_preface_buf((const char *)CHROME_PREFACE, sizeof(CHROME_PREFACE), &res);
/* Chrome : :method(m), :authority(a), :scheme(s), :path(p) */
assert_string_equal(res.pseudo, "m,a,s,p");
}
static void test_chrome_fingerprint_akamai(void **state)
{
(void)state;
h2_result_t res;
h2_parse_preface_buf((const char *)CHROME_PREFACE, sizeof(CHROME_PREFACE), &res);
assert_string_equal(res.fingerprint,
"1:65536,2:0,4:6291456,6:262144|15663105|0|m,a,s,p");
}
static void test_firefox_settings_parsed(void **state)
{
(void)state;
h2_result_t res;
h2_parse_preface_buf((const char *)FIREFOX_PREFACE, sizeof(FIREFOX_PREFACE), &res);
assert_int_equal(res.is_h2, 1);
assert_string_equal(res.settings, "1:65536,4:131072,5:16384");
}
static void test_firefox_pseudo_order(void **state)
{
(void)state;
h2_result_t res;
h2_parse_preface_buf((const char *)FIREFOX_PREFACE, sizeof(FIREFOX_PREFACE), &res);
/* Firefox : :method(m), :path(p), :scheme(s), :authority(a) */
assert_string_equal(res.pseudo, "m,p,s,a");
}
static void test_firefox_fingerprint_akamai(void **state)
{
(void)state;
h2_result_t res;
h2_parse_preface_buf((const char *)FIREFOX_PREFACE, sizeof(FIREFOX_PREFACE), &res);
assert_string_equal(res.fingerprint,
"1:65536,4:131072,5:16384|12517377|0|m,p,s,a");
}
static void test_http1_not_detected(void **state)
{
(void)state;
h2_result_t res;
h2_parse_preface_buf(HTTP1_DATA, strlen(HTTP1_DATA), &res);
assert_int_equal(res.is_h2, 0);
assert_string_equal(res.settings, "");
assert_string_equal(res.fingerprint, "");
}
static void test_empty_buffer_not_detected(void **state)
{
(void)state;
h2_result_t res;
h2_parse_preface_buf("", 0, &res);
assert_int_equal(res.is_h2, 0);
}
static void test_truncated_preface_no_crash(void **state)
{
(void)state;
h2_result_t res;
/* Magic complet mais frame tronquée */
h2_parse_preface_buf((const char *)CHROME_PREFACE, 30, &res);
assert_int_equal(res.is_h2, 0); /* SETTINGS incomplet → pas de fingerprint */
}
static void test_hpack_int_single_byte(void **state)
{
(void)state;
/* Entier 7-bit < 127 → encodé sur 1 octet */
unsigned char buf[] = { 0x82 }; /* 0x80 | 2 → index=2 */
size_t pos = 0;
unsigned int out = 0;
int ok = hpack_int_decode(buf, 1, 7, &pos, &out);
assert_int_equal(ok, 1);
assert_int_equal(out, 2);
assert_int_equal(pos, 1);
}
static void test_hpack_pseudo_table(void **state)
{
(void)state;
assert_int_equal(h2_hpack_pseudo(1), 'a');
assert_int_equal(h2_hpack_pseudo(2), 'm');
assert_int_equal(h2_hpack_pseudo(3), 'm');
assert_int_equal(h2_hpack_pseudo(4), 'p');
assert_int_equal(h2_hpack_pseudo(5), 'p');
assert_int_equal(h2_hpack_pseudo(6), 's');
assert_int_equal(h2_hpack_pseudo(7), 's');
assert_int_equal(h2_hpack_pseudo(8), 0); /* header régulier */
assert_int_equal(h2_hpack_pseudo(62), 0);
}
static void test_pseudo_order_extraction_direct(void **state)
{
(void)state;
/* HPACK block : :method(0x82), :path(0x84), :scheme(0x87), :authority(0x81) */
unsigned char hpack[] = { 0x82, 0x84, 0x87, 0x81 };
char out[16];
h2_extract_pseudo_order(hpack, sizeof(hpack), out);
assert_string_equal(out, "m,p,s,a");
}
static void test_pseudo_order_stops_at_regular_header(void **state)
{
(void)state;
/* :method(0x82), puis header régulier (0x88 = index 8) */
unsigned char hpack[] = { 0x82, 0x88 };
char out[16];
h2_extract_pseudo_order(hpack, sizeof(hpack), out);
assert_string_equal(out, "m");
}
/* ====== main ====== */
int main(void)
{
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_chrome_settings_parsed),
cmocka_unit_test(test_chrome_window_update),
cmocka_unit_test(test_chrome_pseudo_order),
cmocka_unit_test(test_chrome_fingerprint_akamai),
cmocka_unit_test(test_firefox_settings_parsed),
cmocka_unit_test(test_firefox_pseudo_order),
cmocka_unit_test(test_firefox_fingerprint_akamai),
cmocka_unit_test(test_http1_not_detected),
cmocka_unit_test(test_empty_buffer_not_detected),
cmocka_unit_test(test_truncated_preface_no_crash),
cmocka_unit_test(test_hpack_int_single_byte),
cmocka_unit_test(test_hpack_pseudo_table),
cmocka_unit_test(test_pseudo_order_extraction_direct),
cmocka_unit_test(test_pseudo_order_stops_at_regular_header),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}

View File

@ -0,0 +1,226 @@
/*
* test_header_handling.c - Unit tests for header handling (truncation and limits)
*/
#include <stdarg.h>
#include <stddef.h>
#include <setjmp.h>
#include <cmocka.h>
#include <string.h>
#include <stdio.h>
#include <apr_strings.h>
#include <apr_tables.h>
#include <apr_pools.h>
#include <apr_general.h>
/* Mock header truncation function */
static char *truncate_header_value(apr_pool_t *pool, const char *value, int max_len)
{
if (value == NULL) {
return NULL;
}
size_t len = strlen(value);
if ((int)len > max_len) {
return apr_pstrmemdup(pool, value, max_len);
}
return apr_pstrdup(pool, value);
}
/* Mock header matching function */
static int header_name_matches(const char *configured, const char *actual)
{
return strcasecmp(configured, actual) == 0;
}
/* Test: Header value within limit */
static void test_header_truncation_within_limit(void **state)
{
apr_pool_t *pool;
apr_pool_create(&pool, NULL);
const char *value = "short value";
char *result = truncate_header_value(pool, value, 256);
assert_string_equal(result, "short value");
apr_pool_destroy(pool);
}
/* Test: Header value exactly at limit */
static void test_header_truncation_exact_limit(void **state)
{
apr_pool_t *pool;
apr_pool_create(&pool, NULL);
const char *value = "exactly10c";
char *result = truncate_header_value(pool, value, 10);
assert_string_equal(result, "exactly10c");
apr_pool_destroy(pool);
}
/* Test: Header value exceeds limit */
static void test_header_truncation_exceeds_limit(void **state)
{
apr_pool_t *pool;
apr_pool_create(&pool, NULL);
const char *value = "this is a very long header value that should be truncated";
char *result = truncate_header_value(pool, value, 15);
assert_string_equal(result, "this is a very ");
assert_int_equal(strlen(result), 15);
apr_pool_destroy(pool);
}
/* Test: Header value with limit of 1 */
static void test_header_truncation_limit_one(void **state)
{
apr_pool_t *pool;
apr_pool_create(&pool, NULL);
const char *value = "abc";
char *result = truncate_header_value(pool, value, 1);
assert_string_equal(result, "a");
apr_pool_destroy(pool);
}
/* Test: NULL header value */
static void test_header_truncation_null(void **state)
{
apr_pool_t *pool;
apr_pool_create(&pool, NULL);
char *result = truncate_header_value(pool, NULL, 256);
assert_null(result);
apr_pool_destroy(pool);
}
/* Test: Empty header value */
static void test_header_truncation_empty(void **state)
{
apr_pool_t *pool;
apr_pool_create(&pool, NULL);
const char *value = "";
char *result = truncate_header_value(pool, value, 256);
assert_string_equal(result, "");
apr_pool_destroy(pool);
}
/* Test: Header name matching (case-insensitive) */
static void test_header_name_matching_case_insensitive(void **state)
{
assert_true(header_name_matches("X-Request-Id", "x-request-id"));
assert_true(header_name_matches("user-agent", "User-Agent"));
assert_true(header_name_matches("HOST", "host"));
}
/* Test: Header name matching (different headers) */
static void test_header_name_matching_different(void **state)
{
assert_false(header_name_matches("X-Request-Id", "X-Trace-Id"));
assert_false(header_name_matches("Host", "User-Agent"));
}
/* Test: Multiple headers with limit */
static void test_header_count_limit(void **state)
{
apr_pool_t *pool;
apr_pool_create(&pool, NULL);
/* Simulate configured headers */
const char *configured[] = {"X-Request-Id", "X-Trace-Id", "User-Agent", "Referer"};
int configured_count = 4;
int max_headers = 2;
/* Simulate present headers */
const char *present[] = {"X-Request-Id", "User-Agent", "Referer"};
int present_count = 3;
int logged_count = 0;
for (int i = 0; i < configured_count && logged_count < max_headers; i++) {
for (int j = 0; j < present_count; j++) {
if (header_name_matches(configured[i], present[j])) {
logged_count++;
break;
}
}
}
assert_int_equal(logged_count, 2);
apr_pool_destroy(pool);
}
/* Test: Header value with special JSON characters */
static void test_header_value_json_special(void **state)
{
apr_pool_t *pool;
apr_pool_create(&pool, NULL);
const char *value = "test\"value\\with\tspecial";
char *truncated = truncate_header_value(pool, value, 256);
/* Truncation should preserve the value */
assert_string_equal(truncated, "test\"value\\with\tspecial");
apr_pool_destroy(pool);
}
/* Test: Unicode in header value (UTF-8) */
static void test_header_value_unicode(void **state)
{
apr_pool_t *pool;
apr_pool_create(&pool, NULL);
const char *value = "Mozilla/5.0 (compatible; 日本語)";
char *result = truncate_header_value(pool, value, 50);
/* Should be truncated but valid */
assert_non_null(result);
assert_true(strlen(result) <= 50);
apr_pool_destroy(pool);
}
static int group_setup(void **state)
{
(void)state;
return apr_initialize();
}
static int group_teardown(void **state)
{
(void)state;
apr_terminate();
return 0;
}
int main(void)
{
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_header_truncation_within_limit),
cmocka_unit_test(test_header_truncation_exact_limit),
cmocka_unit_test(test_header_truncation_exceeds_limit),
cmocka_unit_test(test_header_truncation_limit_one),
cmocka_unit_test(test_header_truncation_null),
cmocka_unit_test(test_header_truncation_empty),
cmocka_unit_test(test_header_name_matching_case_insensitive),
cmocka_unit_test(test_header_name_matching_different),
cmocka_unit_test(test_header_count_limit),
cmocka_unit_test(test_header_value_json_special),
cmocka_unit_test(test_header_value_unicode),
};
return cmocka_run_group_tests(tests, group_setup, group_teardown);
}

View File

@ -0,0 +1,266 @@
/*
* test_json_serialization.c - Unit tests for JSON serialization
*/
#include <stdarg.h>
#include <stddef.h>
#include <setjmp.h>
#include <cmocka.h>
#include <string.h>
#include <stdio.h>
#include <apr_pools.h>
#include <apr_strings.h>
#include <apr_time.h>
#include <apr_lib.h>
#include <apr_general.h>
typedef struct {
char *data;
size_t len;
size_t cap;
apr_pool_t *pool;
} testbuf_t;
static void testbuf_init(testbuf_t *buf, apr_pool_t *pool, size_t initial_capacity)
{
buf->pool = pool;
buf->cap = initial_capacity;
buf->len = 0;
buf->data = apr_palloc(pool, initial_capacity);
buf->data[0] = '\0';
}
static void testbuf_append(testbuf_t *buf, const char *str, size_t len)
{
if (str == NULL) {
return;
}
if (len == (size_t)-1) {
len = strlen(str);
}
if (buf->len + len + 1 > buf->cap) {
size_t new_cap = (buf->len + len + 1) * 2;
char *new_data = apr_palloc(buf->pool, new_cap);
memcpy(new_data, buf->data, buf->len + 1); /* Copy including null terminator */
buf->data = new_data;
buf->cap = new_cap;
}
memcpy(buf->data + buf->len, str, len);
buf->len += len;
buf->data[buf->len] = '\0';
}
static void testbuf_append_char(testbuf_t *buf, char c)
{
if (buf->len + 2 > buf->cap) {
size_t new_cap = (buf->cap * 2);
char *new_data = apr_palloc(buf->pool, new_cap);
memcpy(new_data, buf->data, buf->len + 1);
buf->data = new_data;
buf->cap = new_cap;
}
buf->data[buf->len++] = c;
buf->data[buf->len] = '\0';
}
/* Mock JSON string escaping function for testing */
static void append_json_string(testbuf_t *buf, const char *str)
{
if (str == NULL) {
return;
}
for (const char *p = str; *p; p++) {
char c = *p;
switch (c) {
case '"': testbuf_append(buf, "\\\"", 2); break;
case '\\': testbuf_append(buf, "\\\\", 2); break;
case '\b': testbuf_append(buf, "\\b", 2); break;
case '\f': testbuf_append(buf, "\\f", 2); break;
case '\n': testbuf_append(buf, "\\n", 2); break;
case '\r': testbuf_append(buf, "\\r", 2); break;
case '\t': testbuf_append(buf, "\\t", 2); break;
default:
if ((unsigned char)c < 0x20) {
char unicode[8];
apr_snprintf(unicode, sizeof(unicode), "\\u%04x", (unsigned char)c);
testbuf_append(buf, unicode, (size_t)-1);
} else {
testbuf_append_char(buf, c);
}
break;
}
}
}
/* Test: Empty string */
static void test_json_escape_empty_string(void **state)
{
apr_pool_t *pool;
testbuf_t buf;
(void)state;
apr_pool_create(&pool, NULL);
testbuf_init(&buf, pool, 256);
append_json_string(&buf, "");
assert_string_equal(buf.data, "");
apr_pool_destroy(pool);
}
/* Test: Simple string without special characters */
static void test_json_escape_simple_string(void **state)
{
apr_pool_t *pool;
testbuf_t buf;
(void)state;
apr_pool_create(&pool, NULL);
testbuf_init(&buf, pool, 256);
append_json_string(&buf, "hello world");
assert_string_equal(buf.data, "hello world");
apr_pool_destroy(pool);
}
/* Test: String with double quotes */
static void test_json_escape_quotes(void **state)
{
apr_pool_t *pool;
testbuf_t buf;
(void)state;
apr_pool_create(&pool, NULL);
testbuf_init(&buf, pool, 256);
append_json_string(&buf, "hello \"world\"");
assert_string_equal(buf.data, "hello \\\"world\\\"");
apr_pool_destroy(pool);
}
/* Test: String with backslashes */
static void test_json_escape_backslashes(void **state)
{
apr_pool_t *pool;
testbuf_t buf;
(void)state;
apr_pool_create(&pool, NULL);
testbuf_init(&buf, pool, 256);
append_json_string(&buf, "path\\to\\file");
assert_string_equal(buf.data, "path\\\\to\\\\file");
apr_pool_destroy(pool);
}
/* Test: String with newlines and tabs */
static void test_json_escape_newlines_tabs(void **state)
{
apr_pool_t *pool;
testbuf_t buf;
(void)state;
apr_pool_create(&pool, NULL);
testbuf_init(&buf, pool, 256);
append_json_string(&buf, "line1\nline2\ttab");
assert_string_equal(buf.data, "line1\\nline2\\ttab");
apr_pool_destroy(pool);
}
/* Test: String with control characters */
static void test_json_escape_control_chars(void **state)
{
apr_pool_t *pool;
testbuf_t buf;
(void)state;
apr_pool_create(&pool, NULL);
testbuf_init(&buf, pool, 256);
/* Test with bell character (0x07) - use octal literal */
append_json_string(&buf, "test\007bell");
/* Should contain unicode escape for bell (0x07) */
assert_true(strstr(buf.data, "\\u0007") != NULL);
apr_pool_destroy(pool);
}
/* Test: NULL string */
static void test_json_escape_null_string(void **state)
{
apr_pool_t *pool;
testbuf_t buf;
(void)state;
apr_pool_create(&pool, NULL);
testbuf_init(&buf, pool, 256);
append_json_string(&buf, NULL);
assert_string_equal(buf.data, "");
apr_pool_destroy(pool);
}
/* Test: Complex user agent string */
static void test_json_escape_user_agent(void **state)
{
apr_pool_t *pool;
testbuf_t buf;
const char *ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) \"Test\"";
(void)state;
apr_pool_create(&pool, NULL);
testbuf_init(&buf, pool, 512);
append_json_string(&buf, ua);
assert_true(strstr(buf.data, "\\\"Test\\\"") != NULL);
apr_pool_destroy(pool);
}
static int group_setup(void **state)
{
(void)state;
return apr_initialize();
}
static int group_teardown(void **state)
{
(void)state;
apr_terminate();
return 0;
}
int main(void)
{
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_json_escape_empty_string),
cmocka_unit_test(test_json_escape_simple_string),
cmocka_unit_test(test_json_escape_quotes),
cmocka_unit_test(test_json_escape_backslashes),
cmocka_unit_test(test_json_escape_newlines_tabs),
cmocka_unit_test(test_json_escape_control_chars),
cmocka_unit_test(test_json_escape_null_string),
cmocka_unit_test(test_json_escape_user_agent),
};
return cmocka_run_group_tests(tests, group_setup, group_teardown);
}

File diff suppressed because it is too large Load Diff