feat: ja4-platform monorepo — 5 services unified, tests & RPM builds standardized
Services: - ja4sentinel: TLS/JA4 fingerprint capture daemon (Go, libpcap) - logcorrelator: JA4 log correlation engine (Go, ClickHouse) - mod_reqin_log: Apache module (C, JSON request logging) - bot_detector: ML bot detection pipeline (Python) - dashboard: FastAPI/Streamlit analytics UI (Python) Shared libraries: - shared/go/ja4common: logger, config, shutdown, ipfilter (Go module) - shared/python/ja4_common: ClickHouseClient, ClickHouseSettings (Python package) - shared/clickhouse/: canonical SQL migrations (10 files) Build & packaging: - Unified 3-stage Dockerfile.package for Go RPMs (el8/el9/el10) - go.work workspace linking sentinel, correlator, ja4common - Makefile with test-all, build-all, rpm-* targets Fixes applied: - go.work: 1.21 → 1.24.6 (required by sentinel) - correlator Dockerfiles: golang:1.21 → golang:1.24 - replace directives in go.mod for ja4common local path - pyproject.toml: setuptools.backends → setuptools.build_meta - Removed static libpcap linking (unavailable on Rocky 9) - Fixed data races in output/writers_test.go (sync.Mutex + atomic.Int32) - Rewrote corrupted test files (logger_test.go × 2) Test coverage: - correlator: 67.1% total (unixsocket 80.5%, config 91.7%, app 83.3%, multi 87.7%, stdout 100%) - sentinel: all 10 packages pass (api, capture, config, fingerprint, ipfilter, logging, output, tlsparse) Documentation: - README.md + docs/ (architecture, development, 5 services, shared libs, DB schema & migrations) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
35
services/mod-reqin-log/.gitignore
vendored
Normal file
35
services/mod-reqin-log/.gitignore
vendored
Normal 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*
|
||||
94
services/mod-reqin-log/.gitlab-ci.yml
Normal file
94
services/mod-reqin-log/.gitlab-ci.yml
Normal 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'"
|
||||
43
services/mod-reqin-log/CMakeLists.txt
Normal file
43
services/mod-reqin-log/CMakeLists.txt
Normal file
@ -0,0 +1,43 @@
|
||||
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})
|
||||
|
||||
# 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)
|
||||
|
||||
# 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
|
||||
)
|
||||
176
services/mod-reqin-log/Dockerfile.package
Normal file
176
services/mod-reqin-log/Dockerfile.package
Normal 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/"]
|
||||
43
services/mod-reqin-log/Dockerfile.tests
Normal file
43
services/mod-reqin-log/Dockerfile.tests
Normal 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", "--output-on-failure"]
|
||||
107
services/mod-reqin-log/LICENSE
Normal file
107
services/mod-reqin-log/LICENSE
Normal 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
|
||||
153
services/mod-reqin-log/Makefile
Normal file
153
services/mod-reqin-log/Makefile
Normal 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)"
|
||||
284
services/mod-reqin-log/README.md
Normal file
284
services/mod-reqin-log/README.md
Normal 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
|
||||
443
services/mod-reqin-log/architecture.yml
Normal file
443
services/mod-reqin-log/architecture.yml
Normal 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
|
||||
|
||||
40
services/mod-reqin-log/conf/mod_reqin_log.conf
Normal file
40
services/mod-reqin-log/conf/mod_reqin_log.conf
Normal 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
|
||||
123
services/mod-reqin-log/mod_reqin_log.spec
Normal file
123
services/mod-reqin-log/mod_reqin_log.spec
Normal 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
|
||||
1033
services/mod-reqin-log/src/mod_reqin_log.c
Normal file
1033
services/mod-reqin-log/src/mod_reqin_log.c
Normal file
File diff suppressed because it is too large
Load Diff
37
services/mod-reqin-log/src/mod_reqin_log.h
Normal file
37
services/mod-reqin-log/src/mod_reqin_log.h
Normal file
@ -0,0 +1,37 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
#endif /* MOD_REQIN_LOG_H */
|
||||
333
services/mod-reqin-log/tests/unit/test_config_parsing.c
Normal file
333
services/mod-reqin-log/tests/unit/test_config_parsing.c
Normal 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);
|
||||
}
|
||||
226
services/mod-reqin-log/tests/unit/test_header_handling.c
Normal file
226
services/mod-reqin-log/tests/unit/test_header_handling.c
Normal 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);
|
||||
}
|
||||
266
services/mod-reqin-log/tests/unit/test_json_serialization.c
Normal file
266
services/mod-reqin-log/tests/unit/test_json_serialization.c
Normal 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);
|
||||
}
|
||||
1141
services/mod-reqin-log/tests/unit/test_module_real.c
Normal file
1141
services/mod-reqin-log/tests/unit/test_module_real.c
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user