Initial commit: mod_reqin_log Apache module
Features: - JSON logging of HTTP requests to Unix domain socket - Configurable HTTP headers logging (flat JSON structure) - Header value truncation and count limits - Automatic reconnect on socket disconnection - Error reporting with throttling Configuration directives: - JsonSockLogEnabled: Enable/disable logging - JsonSockLogSocket: Unix socket path - JsonSockLogHeaders: List of headers to log - JsonSockLogMaxHeaders: Maximum headers to log - JsonSockLogMaxHeaderValueLen: Max header value length - JsonSockLogReconnectInterval: Reconnect delay - JsonSockLogErrorReportInterval: Error log throttle Includes: - Module source code (src/) - Unit and integration tests (tests/, scripts/) - Documentation (README.md, architecture.yml) - Build configuration (CMakeLists.txt, Makefile) - Packaging (deb/rpm) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
202
.github/workflows/ci.yml
vendored
Normal file
202
.github/workflows/ci.yml
vendored
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main, develop ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ main ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# Build on Rocky Linux 8
|
||||||
|
build-rocky-8:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: rockylinux:8
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
dnf install -y epel-release
|
||||||
|
dnf install -y gcc make httpd httpd-devel apr-devel apr-util-devel rpm-build
|
||||||
|
|
||||||
|
- name: Build module
|
||||||
|
run: |
|
||||||
|
make APXS=/usr/bin/apxs
|
||||||
|
|
||||||
|
- name: Verify module
|
||||||
|
run: |
|
||||||
|
ls -la modules/mod_reqin_log.so
|
||||||
|
|
||||||
|
- name: Upload module artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: mod_reqin_log-rocky8
|
||||||
|
path: modules/mod_reqin_log.so
|
||||||
|
|
||||||
|
# Build on Debian
|
||||||
|
build-debian:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: debian:stable
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
apt-get update
|
||||||
|
apt-get install -y build-essential apache2 apache2-dev
|
||||||
|
|
||||||
|
- name: Build module
|
||||||
|
run: |
|
||||||
|
make APXS=/usr/bin/apxs
|
||||||
|
|
||||||
|
- name: Verify module
|
||||||
|
run: |
|
||||||
|
ls -la modules/mod_reqin_log.so
|
||||||
|
|
||||||
|
- name: Upload module artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: mod_reqin_log-debian
|
||||||
|
path: modules/mod_reqin_log.so
|
||||||
|
|
||||||
|
# Unit tests
|
||||||
|
unit-tests:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: debian:stable
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
apt-get update
|
||||||
|
apt-get install -y build-essential cmake libcmocka-dev apache2-dev
|
||||||
|
|
||||||
|
- name: Configure tests
|
||||||
|
run: |
|
||||||
|
mkdir -p build/tests
|
||||||
|
cd build/tests
|
||||||
|
cmake ../../
|
||||||
|
|
||||||
|
- name: Build tests
|
||||||
|
run: |
|
||||||
|
make -C build/tests
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: |
|
||||||
|
make -C build/tests run_tests
|
||||||
|
|
||||||
|
# Build RPM package
|
||||||
|
build-rpm:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: rockylinux:8
|
||||||
|
needs: [build-rocky-8]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
dnf install -y epel-release
|
||||||
|
dnf install -y gcc make httpd httpd-devel apr-devel apr-util-devel rpm-build rpmlint
|
||||||
|
|
||||||
|
- name: Create source tarball
|
||||||
|
run: |
|
||||||
|
tar -czf mod_reqin_log-1.0.0.tar.gz --transform 's,^,mod_reqin_log-1.0.0/,' .
|
||||||
|
|
||||||
|
- name: Setup rpmbuild
|
||||||
|
run: |
|
||||||
|
mkdir -p ~/rpmbuild/{BUILD,RPMS,SOURCES,SPECS,SRPMS}
|
||||||
|
cp mod_reqin_log-1.0.0.tar.gz ~/rpmbuild/SOURCES/
|
||||||
|
cp packaging/rpm/mod_reqin_log.spec ~/rpmbuild/SPECS/
|
||||||
|
|
||||||
|
- name: Build RPM
|
||||||
|
run: |
|
||||||
|
rpmbuild -ba ~/rpmbuild/SPECS/mod_reqin_log.spec
|
||||||
|
|
||||||
|
- name: Upload RPM artifacts
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: rpm-packages
|
||||||
|
path: ~/rpmbuild/RPMS/x86_64/*.rpm
|
||||||
|
|
||||||
|
# Build DEB package
|
||||||
|
build-deb:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: debian:stable
|
||||||
|
needs: [build-debian]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
apt-get update
|
||||||
|
apt-get install -y build-essential apache2 apache2-dev debhelper devscripts dpkg-dev
|
||||||
|
|
||||||
|
- name: Setup package metadata
|
||||||
|
run: |
|
||||||
|
cp -r packaging/deb/* ./debian/
|
||||||
|
echo "1.0.0" > debian/changelog
|
||||||
|
echo "mod_reqin_log (1.0.0) stable; urgency=medium" >> debian/changelog
|
||||||
|
echo "" >> debian/changelog
|
||||||
|
echo " * Initial release" >> debian/changelog
|
||||||
|
echo "" >> debian/changelog
|
||||||
|
echo " -- Developer <dev@example.com> $(date -R)" >> debian/changelog
|
||||||
|
|
||||||
|
- name: Build DEB
|
||||||
|
run: |
|
||||||
|
debuild -us -uc -b
|
||||||
|
|
||||||
|
- name: Upload DEB artifacts
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: deb-packages
|
||||||
|
path: ../*.deb
|
||||||
|
|
||||||
|
# Integration tests
|
||||||
|
integration-tests:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: rockylinux:8
|
||||||
|
needs: [build-rocky-8]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
dnf install -y epel-release
|
||||||
|
dnf install -y gcc make httpd httpd-devel apr-devel apr-util-devel python3 curl
|
||||||
|
|
||||||
|
- name: Build module
|
||||||
|
run: |
|
||||||
|
make APXS=/usr/bin/apxs
|
||||||
|
|
||||||
|
- name: Setup Apache configuration
|
||||||
|
run: |
|
||||||
|
mkdir -p /var/run/mod_reqin_log
|
||||||
|
cp conf/mod_reqin_log.conf /etc/httpd/conf.d/
|
||||||
|
echo "LoadModule reqin_log_module /github/workspace/modules/mod_reqin_log.so" > /etc/httpd/conf.d/00-mod_reqin_log.conf
|
||||||
|
|
||||||
|
- name: Start socket consumer
|
||||||
|
run: |
|
||||||
|
python3 scripts/socket_consumer.py &
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
- name: Start Apache
|
||||||
|
run: |
|
||||||
|
httpd -t
|
||||||
|
httpd -DFOREGROUND &
|
||||||
|
sleep 3
|
||||||
|
|
||||||
|
- name: Run integration tests
|
||||||
|
run: |
|
||||||
|
curl -H "X-Request-Id: test-123" http://localhost/
|
||||||
|
curl -H "X-Trace-Id: trace-456" http://localhost/api
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
- name: Verify logs
|
||||||
|
run: |
|
||||||
|
echo "Integration test completed"
|
||||||
34
.gitignore
vendored
Normal file
34
.gitignore
vendored
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
# 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
|
||||||
36
CMakeLists.txt
Normal file
36
CMakeLists.txt
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
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)
|
||||||
|
|
||||||
|
# Include directories
|
||||||
|
include_directories(${CMOCKA_INCLUDE_DIRS})
|
||||||
|
include_directories(/usr/include/httpd)
|
||||||
|
include_directories(/usr/include/apr-1)
|
||||||
|
|
||||||
|
# Test executable
|
||||||
|
add_executable(test_json_serialization tests/unit/test_json_serialization.c)
|
||||||
|
target_link_libraries(test_json_serialization ${CMOCKA_LIBRARIES} m)
|
||||||
|
|
||||||
|
add_executable(test_header_handling tests/unit/test_header_handling.c)
|
||||||
|
target_link_libraries(test_header_handling ${CMOCKA_LIBRARIES} m)
|
||||||
|
|
||||||
|
add_executable(test_config_parsing tests/unit/test_config_parsing.c)
|
||||||
|
target_link_libraries(test_config_parsing ${CMOCKA_LIBRARIES} m)
|
||||||
|
|
||||||
|
# Enable testing
|
||||||
|
enable_testing()
|
||||||
|
add_test(NAME JsonSerializationTest COMMAND test_json_serialization)
|
||||||
|
add_test(NAME HeaderHandlingTest COMMAND test_header_handling)
|
||||||
|
add_test(NAME ConfigParsingTest COMMAND test_config_parsing)
|
||||||
|
|
||||||
|
# Custom target for running tests
|
||||||
|
add_custom_target(run_tests
|
||||||
|
COMMAND ${CMAKE_CTEST_COMMAND} --output-on-failure
|
||||||
|
DEPENDS test_json_serialization test_header_handling test_config_parsing
|
||||||
|
)
|
||||||
33
Dockerfile
Normal file
33
Dockerfile
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
# Dockerfile for building mod_reqin_log (minimal - no tests)
|
||||||
|
FROM rockylinux:8
|
||||||
|
|
||||||
|
# Install build dependencies
|
||||||
|
RUN dnf install -y epel-release && \
|
||||||
|
dnf install -y \
|
||||||
|
gcc \
|
||||||
|
make \
|
||||||
|
httpd \
|
||||||
|
httpd-devel \
|
||||||
|
apr-devel \
|
||||||
|
apr-util-devel \
|
||||||
|
python3 \
|
||||||
|
curl \
|
||||||
|
redhat-rpm-config \
|
||||||
|
&& dnf clean all
|
||||||
|
|
||||||
|
# Set working directory
|
||||||
|
WORKDIR /build
|
||||||
|
|
||||||
|
# Copy source files
|
||||||
|
COPY src/ src/
|
||||||
|
COPY Makefile Makefile
|
||||||
|
COPY conf/ conf/
|
||||||
|
|
||||||
|
# Build the module
|
||||||
|
RUN make APXS=/usr/bin/apxs
|
||||||
|
|
||||||
|
# Verify module was built
|
||||||
|
RUN ls -la modules/mod_reqin_log.so
|
||||||
|
|
||||||
|
# Default command
|
||||||
|
CMD ["/bin/bash"]
|
||||||
40
Dockerfile.test-socket
Normal file
40
Dockerfile.test-socket
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
# Dockerfile for running Unix socket integration tests
|
||||||
|
FROM rockylinux:8
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
RUN dnf install -y epel-release && \
|
||||||
|
dnf install -y \
|
||||||
|
gcc \
|
||||||
|
make \
|
||||||
|
httpd \
|
||||||
|
httpd-devel \
|
||||||
|
apr-devel \
|
||||||
|
apr-util-devel \
|
||||||
|
python3 \
|
||||||
|
curl \
|
||||||
|
redhat-rpm-config \
|
||||||
|
&& dnf clean all
|
||||||
|
|
||||||
|
# Copy module source
|
||||||
|
COPY src/ src/
|
||||||
|
COPY Makefile Makefile
|
||||||
|
COPY conf/ conf/
|
||||||
|
|
||||||
|
# Build the module
|
||||||
|
RUN make APXS=/usr/bin/apxs
|
||||||
|
|
||||||
|
# Copy test scripts
|
||||||
|
COPY scripts/test_unix_socket.sh /test_unix_socket.sh
|
||||||
|
COPY scripts/socket_listener.py /build/scripts/socket_listener.py
|
||||||
|
RUN chmod +x /test_unix_socket.sh
|
||||||
|
RUN mkdir -p /build/scripts
|
||||||
|
|
||||||
|
# Create document root
|
||||||
|
RUN mkdir -p /var/www/html
|
||||||
|
RUN echo "<html><body><h1>Test</h1></body></html>" > /var/www/html/index.html
|
||||||
|
|
||||||
|
# Set working directory
|
||||||
|
WORKDIR /build
|
||||||
|
|
||||||
|
# Run the test
|
||||||
|
CMD ["/test_unix_socket.sh"]
|
||||||
46
Dockerfile.tests
Normal file
46
Dockerfile.tests
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
# Dockerfile for running unit tests
|
||||||
|
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 \
|
||||||
|
&& 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 && \
|
||||||
|
make && \
|
||||||
|
make install && \
|
||||||
|
cd / && \
|
||||||
|
rm -rf /tmp/cmocka && \
|
||||||
|
dnf remove -y git && \
|
||||||
|
dnf clean all
|
||||||
|
|
||||||
|
WORKDIR /build
|
||||||
|
|
||||||
|
COPY src/ src/
|
||||||
|
COPY tests/ tests/
|
||||||
|
COPY CMakeLists.txt CMakeLists.txt
|
||||||
|
COPY Makefile Makefile
|
||||||
|
|
||||||
|
# Build and run tests
|
||||||
|
RUN mkdir -p build/tests && \
|
||||||
|
cd build/tests && \
|
||||||
|
cmake ../../ && \
|
||||||
|
make
|
||||||
|
|
||||||
|
CMD ["ctest", "--output-on-failure"]
|
||||||
107
LICENSE
Normal file
107
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
|
||||||
86
Makefile
Normal file
86
Makefile
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
# Directories
|
||||||
|
SRC_DIR = src
|
||||||
|
BUILD_DIR = build
|
||||||
|
INSTALL_DIR = modules
|
||||||
|
|
||||||
|
# Source files
|
||||||
|
SRCS = $(SRC_DIR)/mod_reqin_log.c
|
||||||
|
|
||||||
|
# Module name
|
||||||
|
MODULE_NAME = mod_reqin_log
|
||||||
|
|
||||||
|
.PHONY: all clean install uninstall test
|
||||||
|
|
||||||
|
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
|
||||||
|
cp $(BUILD_DIR)/$(MODULE_NAME).so $(DESTDIR)/usr/lib/apache2/modules/
|
||||||
|
@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
|
||||||
|
|
||||||
|
# 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 " help - Show this help message"
|
||||||
|
@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: /)"
|
||||||
259
README.md
Normal file
259
README.md
Normal file
@ -0,0 +1,259 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Apache HTTPD 2.4+
|
||||||
|
- GCC compiler
|
||||||
|
- APR development libraries
|
||||||
|
- Apache development headers (`httpd-devel` or `apache2-dev`)
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### Build from Source
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clone or extract the source
|
||||||
|
cd mod_reqin_log
|
||||||
|
|
||||||
|
# Build the module
|
||||||
|
make
|
||||||
|
|
||||||
|
# Install (requires root privileges)
|
||||||
|
sudo make install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using Package Manager
|
||||||
|
|
||||||
|
#### RPM (Rocky Linux 8+)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build RPM package
|
||||||
|
rpmbuild -ba packaging/rpm/mod_reqin_log.spec
|
||||||
|
|
||||||
|
# Install the package
|
||||||
|
sudo rpm -ivh ~/rpmbuild/RPMS/x86_64/mod_reqin_log-1.0.0-1.el8.x86_64.rpm
|
||||||
|
```
|
||||||
|
|
||||||
|
#### DEB (Debian/Ubuntu)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build DEB package
|
||||||
|
cd packaging/deb
|
||||||
|
debuild -us -uc
|
||||||
|
|
||||||
|
# Install the package
|
||||||
|
sudo dpkg -i ../libapache2-mod-reqin-log_1.0.0_amd64.deb
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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/mod_reqin_log.sock"
|
||||||
|
|
||||||
|
# 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
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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 | Nanoseconds since epoch |
|
||||||
|
| `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 |
|
||||||
|
| `headers` | Object | Configured HTTP headers |
|
||||||
|
|
||||||
|
## 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 = "/var/run/mod_reqin_log.sock"
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
os.chmod(SOCKET_PATH, 0o666)
|
||||||
|
|
||||||
|
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()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
⚠️ **Important**: This module logs all HTTP requests transparently.
|
||||||
|
|
||||||
|
- **Do not log sensitive headers**: Avoid including `Authorization`, `Cookie`, `X-Api-Key`, or other sensitive headers in `JsonSockLogHeaders`
|
||||||
|
- **Socket permissions**: Ensure the Unix socket has appropriate file permissions
|
||||||
|
- **Log consumer security**: Protect the socket consumer from unauthorized access
|
||||||
|
|
||||||
|
## 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/mod_reqin_log.sock
|
||||||
|
```
|
||||||
|
|
||||||
|
- 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
|
||||||
|
# Install test dependencies
|
||||||
|
sudo dnf install cmocka-devel # Rocky Linux
|
||||||
|
sudo apt install libcmocka-dev # Debian/Ubuntu
|
||||||
|
|
||||||
|
# Build and run tests
|
||||||
|
mkdir build && cd build
|
||||||
|
cmake ..
|
||||||
|
make test
|
||||||
|
```
|
||||||
|
|
||||||
|
### Integration Testing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start socket consumer
|
||||||
|
python3 scripts/socket_consumer.py &
|
||||||
|
|
||||||
|
# Start Apache with module enabled
|
||||||
|
sudo systemctl start httpd
|
||||||
|
|
||||||
|
# Send test requests
|
||||||
|
curl -H "X-Request-Id: test-123" http://localhost/
|
||||||
|
|
||||||
|
# Check consumer output
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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
|
||||||
396
architecture.yml
Normal file
396
architecture.yml
Normal file
@ -0,0 +1,396 @@
|
|||||||
|
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
|
||||||
|
target:
|
||||||
|
server: apache-httpd
|
||||||
|
version: "2.4"
|
||||||
|
os: rocky-linux-8+
|
||||||
|
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.
|
||||||
|
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
|
||||||
|
hooks:
|
||||||
|
- name: register_hooks
|
||||||
|
responsibilities:
|
||||||
|
- Register post_read_request hook for logging at request reception.
|
||||||
|
- Initialize per-process Unix socket connection if enabled.
|
||||||
|
- name: child_init
|
||||||
|
responsibilities:
|
||||||
|
- Initialize module state for each Apache child process.
|
||||||
|
- Attempt initial non-blocking connection to Unix socket if configured.
|
||||||
|
- name: child_exit
|
||||||
|
responsibilities:
|
||||||
|
- Cleanly close Unix socket file descriptor if open.
|
||||||
|
- name: post_read_request
|
||||||
|
responsibilities:
|
||||||
|
- 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.
|
||||||
|
data_model:
|
||||||
|
json_line:
|
||||||
|
description: >
|
||||||
|
One JSON object per HTTP request, serialized on a single line and
|
||||||
|
terminated by "\n".
|
||||||
|
fields:
|
||||||
|
- name: time
|
||||||
|
type: string
|
||||||
|
format: iso8601-with-timezone
|
||||||
|
example: "2026-02-26T11:59:30Z"
|
||||||
|
- name: timestamp
|
||||||
|
type: integer
|
||||||
|
unit: nanoseconds
|
||||||
|
description: >
|
||||||
|
Monotonic or wall-clock based timestamp in nanoseconds since an
|
||||||
|
implementation-defined epoch (stable enough for ordering and latency analysis).
|
||||||
|
- 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
|
||||||
|
example: "/foo/bar"
|
||||||
|
- name: host
|
||||||
|
type: string
|
||||||
|
example: "example.com"
|
||||||
|
- name: http_version
|
||||||
|
type: string
|
||||||
|
example: "HTTP/1.1"
|
||||||
|
- name: headers
|
||||||
|
type: object
|
||||||
|
description: >
|
||||||
|
Flattened headers from the configured header list. Keys are derived
|
||||||
|
from configured header names prefixed with 'header_'.
|
||||||
|
key_pattern: "header_<configured_header_name>"
|
||||||
|
example:
|
||||||
|
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/mod_reqin_log.sock"
|
||||||
|
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"]
|
||||||
|
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: 10
|
||||||
|
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:
|
||||||
|
- No built-in blacklist; admin is fully responsible for excluding
|
||||||
|
sensitive headers (Authorization, Cookie, etc.).
|
||||||
|
- If a configured header is absent in a request, the corresponding
|
||||||
|
JSON key may be omitted or set to null (implementation choice, but
|
||||||
|
must be consistent).
|
||||||
|
- Header values are truncated to JsonSockLogMaxHeaderValueLen characters.
|
||||||
|
|
||||||
|
io:
|
||||||
|
socket:
|
||||||
|
type: unix-domain
|
||||||
|
mode: client
|
||||||
|
path_source: JsonSockLogSocket
|
||||||
|
connection:
|
||||||
|
persistence: true
|
||||||
|
non_blocking: true
|
||||||
|
lifecycle:
|
||||||
|
open:
|
||||||
|
- Attempt initial connection during child_init if enabled.
|
||||||
|
- On first log attempt after reconnect interval expiry if not yet connected.
|
||||||
|
failure:
|
||||||
|
- On connection failure, mark socket as unavailable.
|
||||||
|
- Do not block the worker process.
|
||||||
|
reconnect:
|
||||||
|
strategy: time-based
|
||||||
|
interval_seconds: "@config.JsonSockLogReconnectInterval"
|
||||||
|
trigger: >
|
||||||
|
When a request arrives and the last connect attempt time is older
|
||||||
|
than reconnect interval, a new connect is attempted.
|
||||||
|
write:
|
||||||
|
format: "json_object + '\\n'"
|
||||||
|
mode: non-blocking
|
||||||
|
error_handling:
|
||||||
|
on_eagain:
|
||||||
|
action: drop-current-log-line
|
||||||
|
note: do not retry for this request.
|
||||||
|
on_epipe_or_conn_reset:
|
||||||
|
action:
|
||||||
|
- close_socket
|
||||||
|
- mark_unavailable
|
||||||
|
- schedule_reconnect
|
||||||
|
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 does not anonymize IPs nor scrub headers; it is intentionally
|
||||||
|
transparent. Data protection and header choices are delegated to configuration.
|
||||||
|
- No requests are rejected due to logging failures.
|
||||||
|
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:
|
||||||
|
focus:
|
||||||
|
- JSON serialization with header truncation and header count limits.
|
||||||
|
- Directive parsing and configuration merging (global scope).
|
||||||
|
- Error-handling branches for non-blocking write and reconnect logic.
|
||||||
|
integration_tests:
|
||||||
|
env:
|
||||||
|
server: apache-httpd 2.4
|
||||||
|
os: rocky-linux-8+
|
||||||
|
log_consumer: simple Unix socket server capturing JSON lines
|
||||||
|
scenarios:
|
||||||
|
- name: basic_logging
|
||||||
|
description: >
|
||||||
|
With JsonSockLogEnabled On and valid socket, verify that each request
|
||||||
|
produces a valid JSON line with expected fields.
|
||||||
|
- name: header_limits
|
||||||
|
description: >
|
||||||
|
Configure more headers than JsonSockLogMaxHeaders and verify only
|
||||||
|
the first N are logged and values are truncated according to
|
||||||
|
JsonSockLogMaxHeaderValueLen.
|
||||||
|
- name: socket_unavailable_on_start
|
||||||
|
description: >
|
||||||
|
Start Apache with JsonSockLogEnabled On but socket not yet created;
|
||||||
|
verify periodic reconnect attempts and throttled error logging.
|
||||||
|
- name: runtime_socket_loss
|
||||||
|
description: >
|
||||||
|
Drop the Unix socket while traffic is ongoing; verify that log lines
|
||||||
|
are dropped, worker threads are not blocked, and reconnect attempts
|
||||||
|
resume once the socket reappears.
|
||||||
|
|
||||||
|
|
||||||
|
ci:
|
||||||
|
strategy:
|
||||||
|
description: >
|
||||||
|
All builds, tests and packaging are executed inside Docker containers.
|
||||||
|
The host only needs Docker and the CI runner.
|
||||||
|
tools:
|
||||||
|
orchestrator: "to-define (GitLab CI / GitHub Actions / autre)"
|
||||||
|
container_engine: docker
|
||||||
|
stages:
|
||||||
|
- name: build
|
||||||
|
description: >
|
||||||
|
Compile mod_reqin_log as an Apache 2.4 module inside Docker images
|
||||||
|
dedicated to each target distribution.
|
||||||
|
jobs:
|
||||||
|
- name: build-rocky-8
|
||||||
|
image: "rockylinux:8"
|
||||||
|
steps:
|
||||||
|
- install_deps:
|
||||||
|
- gcc
|
||||||
|
- make
|
||||||
|
- httpd
|
||||||
|
- httpd-devel
|
||||||
|
- apr-devel
|
||||||
|
- apr-util-devel
|
||||||
|
- rpm-build
|
||||||
|
- build_module:
|
||||||
|
command: "apxs -c -i src/mod_reqin_log.c"
|
||||||
|
- name: build-debian
|
||||||
|
image: "debian:stable"
|
||||||
|
steps:
|
||||||
|
- install_deps:
|
||||||
|
- build-essential
|
||||||
|
- apache2
|
||||||
|
- apache2-dev
|
||||||
|
- debhelper
|
||||||
|
- build_module:
|
||||||
|
command: "apxs -c -i src/mod_reqin_log.c"
|
||||||
|
|
||||||
|
- name: test
|
||||||
|
description: >
|
||||||
|
Run unit tests (C) and integration tests (Apache + Unix socket consumer)
|
||||||
|
inside Docker containers.
|
||||||
|
jobs:
|
||||||
|
- name: unit-tests
|
||||||
|
image: "debian:stable"
|
||||||
|
steps:
|
||||||
|
- install_test_deps:
|
||||||
|
- build-essential
|
||||||
|
- cmake
|
||||||
|
- "test-framework (à définir: cmocka, criterion, ...)"
|
||||||
|
- run_tests:
|
||||||
|
command: "ctest || make test"
|
||||||
|
- name: integration-tests-rocky-8
|
||||||
|
image: "rockylinux:8"
|
||||||
|
steps:
|
||||||
|
- prepare_apache_and_module
|
||||||
|
- start_unix_socket_consumer
|
||||||
|
- run_http_scenarios:
|
||||||
|
description: >
|
||||||
|
Validate JSON logs, header limits, socket loss and reconnect
|
||||||
|
behaviour using curl/ab/siege or similar tools.
|
||||||
|
|
||||||
|
- name: package
|
||||||
|
description: >
|
||||||
|
Build RPM and DEB packages for mod_reqin_log inside Docker.
|
||||||
|
jobs:
|
||||||
|
- name: rpm-rocky-8
|
||||||
|
image: "rockylinux:8"
|
||||||
|
steps:
|
||||||
|
- install_deps:
|
||||||
|
- rpm-build
|
||||||
|
- rpmlint
|
||||||
|
- "build deps same as 'build-rocky-8'"
|
||||||
|
- build_rpm:
|
||||||
|
spec_file: "packaging/rpm/mod_reqin_log.spec"
|
||||||
|
command: "rpmbuild -ba packaging/rpm/mod_reqin_log.spec"
|
||||||
|
- artifacts:
|
||||||
|
paths:
|
||||||
|
- "dist/rpm/**/*.rpm"
|
||||||
|
- name: deb-debian
|
||||||
|
image: "debian:stable"
|
||||||
|
steps:
|
||||||
|
- install_deps:
|
||||||
|
- devscripts
|
||||||
|
- debhelper
|
||||||
|
- dpkg-dev
|
||||||
|
- "build deps same as 'build-debian'"
|
||||||
|
- build_deb:
|
||||||
|
command: |
|
||||||
|
cd packaging/deb
|
||||||
|
debuild -us -uc
|
||||||
|
- artifacts:
|
||||||
|
paths:
|
||||||
|
- "dist/deb/**/*.deb"
|
||||||
|
|
||||||
|
artifacts:
|
||||||
|
retention:
|
||||||
|
policy: "keep build logs and packages long enough for debugging (to define)"
|
||||||
|
outputs:
|
||||||
|
- type: module
|
||||||
|
path: "dist/modules/mod_reqin_log.so"
|
||||||
|
- type: rpm
|
||||||
|
path: "dist/rpm/"
|
||||||
|
- type: deb
|
||||||
|
path: "dist/deb/"
|
||||||
|
|
||||||
27
conf/mod_reqin_log.conf
Normal file
27
conf/mod_reqin_log.conf
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
# 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
|
||||||
|
JsonSockLogSocket "/var/run/mod_reqin_log.sock"
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# Maximum number of headers to log (from the configured list)
|
||||||
|
JsonSockLogMaxHeaders 10
|
||||||
|
|
||||||
|
# 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
|
||||||
1
packaging/deb/compat
Normal file
1
packaging/deb/compat
Normal file
@ -0,0 +1 @@
|
|||||||
|
10
|
||||||
27
packaging/deb/control
Normal file
27
packaging/deb/control
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
Source: mod-reqin-log
|
||||||
|
Section: web
|
||||||
|
Priority: optional
|
||||||
|
Maintainer: Developer <dev@example.com>
|
||||||
|
Build-Depends: debhelper (>= 10),
|
||||||
|
apache2-dev,
|
||||||
|
apache2,
|
||||||
|
build-essential,
|
||||||
|
pkg-config
|
||||||
|
Standards-Version: 4.5.0
|
||||||
|
Homepage: https://github.com/example/mod_reqin_log
|
||||||
|
|
||||||
|
Package: libapache2-mod-reqin-log
|
||||||
|
Architecture: any
|
||||||
|
Depends: apache2, ${shlibs:Depends}, ${misc:Depends}
|
||||||
|
Description: Apache HTTPD module for logging HTTP requests as JSON to Unix socket
|
||||||
|
mod_reqin_log is an Apache HTTPD 2.4 module that logs all incoming HTTP requests
|
||||||
|
as JSON lines to a Unix domain socket. The logging occurs at request reception
|
||||||
|
time (post_read_request phase), capturing input-side data without waiting for
|
||||||
|
application processing.
|
||||||
|
.
|
||||||
|
Features:
|
||||||
|
- Non-blocking I/O to avoid stalling worker processes
|
||||||
|
- Configurable header logging with truncation support
|
||||||
|
- Automatic reconnection to Unix socket on failure
|
||||||
|
- Throttled error reporting to Apache error_log
|
||||||
|
- Compatible with prefork, worker, and event MPMs
|
||||||
2
packaging/deb/install
Normal file
2
packaging/deb/install
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
usr/lib/apache2/modules/mod_reqin_log.so
|
||||||
|
etc/apache2/conf-available/mod_reqin_log.conf
|
||||||
24
packaging/deb/rules
Normal file
24
packaging/deb/rules
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
#!/usr/bin/make -f
|
||||||
|
|
||||||
|
%:
|
||||||
|
dh $@
|
||||||
|
|
||||||
|
override_dh_auto_build:
|
||||||
|
$(MAKE) APXS=/usr/bin/apxs
|
||||||
|
|
||||||
|
override_dh_auto_install:
|
||||||
|
$(MAKE) install DESTDIR=$(CURDIR)/debian/libapache2-mod-reqin-log APXS=/usr/bin/apxs
|
||||||
|
install -d $(CURDIR)/debian/libapache2-mod-reqin-log/etc/apache2/conf-available/
|
||||||
|
install -m 644 conf/mod_reqin_log.conf $(CURDIR)/debian/libapache2-mod-reqin-log/etc/apache2/conf-available/mod_reqin_log.conf
|
||||||
|
|
||||||
|
override_dh_auto_clean:
|
||||||
|
$(MAKE) clean || true
|
||||||
|
dh_auto_clean
|
||||||
|
|
||||||
|
override_dh_strip_nondeterminism:
|
||||||
|
# Nothing to strip
|
||||||
|
|
||||||
|
override_dh_install:
|
||||||
|
dh_install
|
||||||
|
install -d $(CURDIR)/debian/libapache2-mod-reqin-log/usr/lib/apache2/modules/
|
||||||
|
install -m 755 .libs/mod_reqin_log.so $(CURDIR)/debian/libapache2-mod-reqin-log/usr/lib/apache2/modules/
|
||||||
53
packaging/rpm/mod_reqin_log.spec
Normal file
53
packaging/rpm/mod_reqin_log.spec
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
Name: mod_reqin_log
|
||||||
|
Version: 1.0.0
|
||||||
|
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
|
||||||
|
Source0: %{name}-%{version}.tar.gz
|
||||||
|
|
||||||
|
BuildRequires: gcc
|
||||||
|
BuildRequires: make
|
||||||
|
BuildRequires: httpd
|
||||||
|
BuildRequires: httpd-devel
|
||||||
|
BuildRequires: apr-devel
|
||||||
|
BuildRequires: apr-util-devel
|
||||||
|
|
||||||
|
Requires: httpd
|
||||||
|
|
||||||
|
%description
|
||||||
|
mod_reqin_log is an Apache HTTPD 2.4 module that logs all incoming HTTP requests
|
||||||
|
as JSON lines to a Unix domain socket. The logging occurs at request reception
|
||||||
|
time (post_read_request phase), capturing input-side data without waiting for
|
||||||
|
application processing.
|
||||||
|
|
||||||
|
Features:
|
||||||
|
- Non-blocking I/O to avoid stalling worker processes
|
||||||
|
- Configurable header logging with truncation support
|
||||||
|
- Automatic reconnection to Unix socket on failure
|
||||||
|
- Throttled error reporting to Apache error_log
|
||||||
|
- Compatible with prefork, worker, and event MPMs
|
||||||
|
|
||||||
|
%prep
|
||||||
|
%setup -q
|
||||||
|
|
||||||
|
%build
|
||||||
|
%{__make} %{?_smp_mflags} APXS=%{_bindir}/apxs
|
||||||
|
|
||||||
|
%install
|
||||||
|
%{__make} install DESTDIR=%{buildroot} APXS=%{_bindir}/apxs
|
||||||
|
|
||||||
|
# Install configuration file
|
||||||
|
mkdir -p %{buildroot}%{_sysconfdir}/httpd/conf.d
|
||||||
|
install -m 644 conf/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 README.md
|
||||||
|
%license LICENSE
|
||||||
|
|
||||||
|
%changelog
|
||||||
|
* Thu Feb 26 2026 Developer <dev@example.com> - 1.0.0-1
|
||||||
|
- Initial package release
|
||||||
27
scripts/build.sh
Executable file
27
scripts/build.sh
Executable file
@ -0,0 +1,27 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# build.sh - Build mod_reqin_log in Docker
|
||||||
|
#
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
IMAGE_NAME="mod_reqin_log-build"
|
||||||
|
|
||||||
|
echo "Building Docker image..."
|
||||||
|
docker build -t "$IMAGE_NAME" "$SCRIPT_DIR/.."
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Build complete. Extracting module..."
|
||||||
|
|
||||||
|
# Create dist directory
|
||||||
|
mkdir -p "$SCRIPT_DIR/../dist"
|
||||||
|
|
||||||
|
# Extract the built module from container
|
||||||
|
docker run --rm -v "$SCRIPT_DIR/../dist:/output" "$IMAGE_NAME" cp /build/modules/mod_reqin_log.so /output/
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Module built successfully: $SCRIPT_DIR/../dist/mod_reqin_log.so"
|
||||||
|
echo ""
|
||||||
|
echo "To test the module:"
|
||||||
|
echo " docker run --rm -v \$PWD/dist:/modules mod_reqin_log-build httpd -t -C 'LoadModule reqin_log_module /modules/mod_reqin_log.so'"
|
||||||
267
scripts/run_integration_tests.sh
Executable file
267
scripts/run_integration_tests.sh
Executable file
@ -0,0 +1,267 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# run_integration_tests.sh - Integration test script for mod_reqin_log
|
||||||
|
#
|
||||||
|
# This script runs integration tests for the mod_reqin_log module.
|
||||||
|
# It requires:
|
||||||
|
# - Apache HTTPD with the module loaded
|
||||||
|
# - A running socket consumer
|
||||||
|
#
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
SOCKET_PATH="/tmp/mod_reqin_log.sock"
|
||||||
|
LOG_FILE="/tmp/mod_reqin_log_test.log"
|
||||||
|
APACHE_URL="${APACHE_URL:-http://localhost}"
|
||||||
|
|
||||||
|
# Colors for output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Counters
|
||||||
|
TESTS_RUN=0
|
||||||
|
TESTS_PASSED=0
|
||||||
|
TESTS_FAILED=0
|
||||||
|
|
||||||
|
log_info() {
|
||||||
|
echo -e "${YELLOW}[INFO]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
log_pass() {
|
||||||
|
echo -e "${GREEN}[PASS]${NC} $1"
|
||||||
|
((TESTS_PASSED++))
|
||||||
|
}
|
||||||
|
|
||||||
|
log_fail() {
|
||||||
|
echo -e "${RED}[FAIL]${NC} $1"
|
||||||
|
((TESTS_FAILED++))
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
log_info "Cleaning up..."
|
||||||
|
rm -f "$LOG_FILE"
|
||||||
|
rm -f "$SOCKET_PATH"
|
||||||
|
}
|
||||||
|
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
# Check prerequisites
|
||||||
|
check_prerequisites() {
|
||||||
|
log_info "Checking prerequisites..."
|
||||||
|
|
||||||
|
if ! command -v curl &> /dev/null; then
|
||||||
|
echo "Error: curl is required but not installed."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v python3 &> /dev/null; then
|
||||||
|
echo "Error: python3 is required but not installed."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Start socket consumer
|
||||||
|
start_consumer() {
|
||||||
|
log_info "Starting socket consumer..."
|
||||||
|
python3 "$SCRIPT_DIR/socket_consumer.py" "$SOCKET_PATH" -o "$LOG_FILE" &
|
||||||
|
CONSUMER_PID=$!
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
if ! kill -0 $CONSUMER_PID 2>/dev/null; then
|
||||||
|
echo "Error: Failed to start socket consumer"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Stop socket consumer
|
||||||
|
stop_consumer() {
|
||||||
|
log_info "Stopping socket consumer..."
|
||||||
|
if [ -n "$CONSUMER_PID" ]; then
|
||||||
|
kill $CONSUMER_PID 2>/dev/null || true
|
||||||
|
wait $CONSUMER_PID 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Test: Basic request logging
|
||||||
|
test_basic_logging() {
|
||||||
|
((TESTS_RUN++))
|
||||||
|
log_info "Test: Basic request logging"
|
||||||
|
|
||||||
|
curl -s "$APACHE_URL/" > /dev/null
|
||||||
|
sleep 1
|
||||||
|
|
||||||
|
if grep -q '"method":"GET"' "$LOG_FILE" 2>/dev/null; then
|
||||||
|
log_pass "Basic logging test"
|
||||||
|
else
|
||||||
|
log_fail "Basic logging test - No GET method found in logs"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Test: Custom header logging
|
||||||
|
test_custom_headers() {
|
||||||
|
((TESTS_RUN++))
|
||||||
|
log_info "Test: Custom header logging"
|
||||||
|
|
||||||
|
curl -s -H "X-Request-Id: test-12345" "$APACHE_URL/" > /dev/null
|
||||||
|
sleep 1
|
||||||
|
|
||||||
|
if grep -q '"header_X-Request-Id":"test-12345"' "$LOG_FILE" 2>/dev/null; then
|
||||||
|
log_pass "Custom header logging test"
|
||||||
|
else
|
||||||
|
log_fail "Custom header logging test - X-Request-Id not found in logs"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Test: Multiple headers
|
||||||
|
test_multiple_headers() {
|
||||||
|
((TESTS_RUN++))
|
||||||
|
log_info "Test: Multiple headers"
|
||||||
|
|
||||||
|
curl -s \
|
||||||
|
-H "X-Request-Id: req-abc" \
|
||||||
|
-H "X-Trace-Id: trace-xyz" \
|
||||||
|
"$APACHE_URL/" > /dev/null
|
||||||
|
sleep 1
|
||||||
|
|
||||||
|
local found_request_id=$(grep -c '"header_X-Request-Id":"req-abc"' "$LOG_FILE" 2>/dev/null || echo 0)
|
||||||
|
local found_trace_id=$(grep -c '"header_X-Trace-Id":"trace-xyz"' "$LOG_FILE" 2>/dev/null || echo 0)
|
||||||
|
|
||||||
|
if [ "$found_request_id" -gt 0 ] && [ "$found_trace_id" -gt 0 ]; then
|
||||||
|
log_pass "Multiple headers test"
|
||||||
|
else
|
||||||
|
log_fail "Multiple headers test - Not all headers found"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Test: JSON format validation
|
||||||
|
test_json_format() {
|
||||||
|
((TESTS_RUN++))
|
||||||
|
log_info "Test: JSON format validation"
|
||||||
|
|
||||||
|
curl -s "$APACHE_URL/" > /dev/null
|
||||||
|
sleep 1
|
||||||
|
|
||||||
|
# Get last line and validate JSON
|
||||||
|
local last_line=$(tail -1 "$LOG_FILE" 2>/dev/null | sed 's/^\[.*\] //')
|
||||||
|
|
||||||
|
if echo "$last_line" | python3 -m json.tool > /dev/null 2>&1; then
|
||||||
|
log_pass "JSON format validation test"
|
||||||
|
else
|
||||||
|
log_fail "JSON format validation test - Invalid JSON format"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Test: Required fields presence
|
||||||
|
test_required_fields() {
|
||||||
|
((TESTS_RUN++))
|
||||||
|
log_info "Test: Required fields presence"
|
||||||
|
|
||||||
|
curl -s "$APACHE_URL/" > /dev/null
|
||||||
|
sleep 1
|
||||||
|
|
||||||
|
local last_line=$(tail -1 "$LOG_FILE" 2>/dev/null | sed 's/^\[.*\] //')
|
||||||
|
|
||||||
|
local required_fields=("time" "timestamp" "src_ip" "dst_ip" "method" "path" "host")
|
||||||
|
local all_present=true
|
||||||
|
|
||||||
|
for field in "${required_fields[@]}"; do
|
||||||
|
if ! echo "$last_line" | grep -q "\"$field\":"; then
|
||||||
|
all_present=false
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if $all_present; then
|
||||||
|
log_pass "Required fields presence test"
|
||||||
|
else
|
||||||
|
log_fail "Required fields presence test - Missing required fields"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Test: HTTP method variations
|
||||||
|
test_method_variations() {
|
||||||
|
((TESTS_RUN++))
|
||||||
|
log_info "Test: HTTP method variations"
|
||||||
|
|
||||||
|
curl -s -X POST "$APACHE_URL/" > /dev/null
|
||||||
|
curl -s -X PUT "$APACHE_URL/" > /dev/null
|
||||||
|
curl -s -X DELETE "$APACHE_URL/" > /dev/null
|
||||||
|
sleep 1
|
||||||
|
|
||||||
|
local found_post=$(grep -c '"method":"POST"' "$LOG_FILE" 2>/dev/null || echo 0)
|
||||||
|
local found_put=$(grep -c '"method":"PUT"' "$LOG_FILE" 2>/dev/null || echo 0)
|
||||||
|
local found_delete=$(grep -c '"method":"DELETE"' "$LOG_FILE" 2>/dev/null || echo 0)
|
||||||
|
|
||||||
|
if [ "$found_post" -gt 0 ] && [ "$found_put" -gt 0 ] && [ "$found_delete" -gt 0 ]; then
|
||||||
|
log_pass "HTTP method variations test"
|
||||||
|
else
|
||||||
|
log_fail "HTTP method variations test - Not all methods found"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Test: Path logging
|
||||||
|
test_path_logging() {
|
||||||
|
((TESTS_RUN++))
|
||||||
|
log_info "Test: Path logging"
|
||||||
|
|
||||||
|
curl -s "$APACHE_URL/api/users" > /dev/null
|
||||||
|
curl -s "$APACHE_URL/foo/bar/baz" > /dev/null
|
||||||
|
sleep 1
|
||||||
|
|
||||||
|
local found_api=$(grep -c '"path":"/api/users"' "$LOG_FILE" 2>/dev/null || echo 0)
|
||||||
|
local found_foo=$(grep -c '"path":"/foo/bar/baz"' "$LOG_FILE" 2>/dev/null || echo 0)
|
||||||
|
|
||||||
|
if [ "$found_api" -gt 0 ] && [ "$found_foo" -gt 0 ]; then
|
||||||
|
log_pass "Path logging test"
|
||||||
|
else
|
||||||
|
log_fail "Path logging test - Not all paths found"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main test runner
|
||||||
|
main() {
|
||||||
|
echo "========================================"
|
||||||
|
echo "mod_reqin_log Integration Tests"
|
||||||
|
echo "========================================"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
check_prerequisites
|
||||||
|
start_consumer
|
||||||
|
|
||||||
|
# Give Apache time to connect to socket
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
# Run tests
|
||||||
|
test_basic_logging
|
||||||
|
test_custom_headers
|
||||||
|
test_multiple_headers
|
||||||
|
test_json_format
|
||||||
|
test_required_fields
|
||||||
|
test_method_variations
|
||||||
|
test_path_logging
|
||||||
|
|
||||||
|
# Stop consumer
|
||||||
|
stop_consumer
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
echo ""
|
||||||
|
echo "========================================"
|
||||||
|
echo "Test Summary"
|
||||||
|
echo "========================================"
|
||||||
|
echo "Tests run: $TESTS_RUN"
|
||||||
|
echo -e "Tests passed: ${GREEN}$TESTS_PASSED${NC}"
|
||||||
|
echo -e "Tests failed: ${RED}$TESTS_FAILED${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if [ $TESTS_FAILED -gt 0 ]; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${GREEN}All tests passed!${NC}"
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
185
scripts/socket_consumer.py
Executable file
185
scripts/socket_consumer.py
Executable file
@ -0,0 +1,185 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
socket_consumer.py - Unix socket consumer for mod_reqin_log
|
||||||
|
|
||||||
|
This script creates a Unix domain socket server that receives JSON log lines
|
||||||
|
from the mod_reqin_log Apache module. It is primarily used for testing and
|
||||||
|
development purposes.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python3 socket_consumer.py [socket_path]
|
||||||
|
|
||||||
|
Example:
|
||||||
|
python3 socket_consumer.py /var/run/mod_reqin_log.sock
|
||||||
|
"""
|
||||||
|
|
||||||
|
import socket
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import signal
|
||||||
|
import argparse
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# Default socket path
|
||||||
|
DEFAULT_SOCKET_PATH = "/tmp/mod_reqin_log.sock"
|
||||||
|
|
||||||
|
# Global flag for graceful shutdown
|
||||||
|
shutdown_requested = False
|
||||||
|
|
||||||
|
|
||||||
|
def signal_handler(signum, frame):
|
||||||
|
"""Handle shutdown signals gracefully."""
|
||||||
|
global shutdown_requested
|
||||||
|
shutdown_requested = True
|
||||||
|
print("\nShutdown requested, finishing current operations...")
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args():
|
||||||
|
"""Parse command line arguments."""
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Unix socket consumer for mod_reqin_log"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"socket_path",
|
||||||
|
nargs="?",
|
||||||
|
default=DEFAULT_SOCKET_PATH,
|
||||||
|
help=f"Path to Unix socket (default: {DEFAULT_SOCKET_PATH})"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-q", "--quiet",
|
||||||
|
action="store_true",
|
||||||
|
help="Suppress log output"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-o", "--output",
|
||||||
|
type=str,
|
||||||
|
help="Write logs to file instead of stdout"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--validate-json",
|
||||||
|
action="store_true",
|
||||||
|
help="Validate JSON and pretty-print"
|
||||||
|
)
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def create_socket(socket_path):
|
||||||
|
"""Create and bind Unix domain socket."""
|
||||||
|
# Remove existing socket file
|
||||||
|
if os.path.exists(socket_path):
|
||||||
|
os.remove(socket_path)
|
||||||
|
|
||||||
|
# Create socket
|
||||||
|
server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||||
|
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
|
server.bind(socket_path)
|
||||||
|
server.listen(5)
|
||||||
|
|
||||||
|
# Set permissions (allow Apache to connect)
|
||||||
|
os.chmod(socket_path, 0o666)
|
||||||
|
|
||||||
|
return server
|
||||||
|
|
||||||
|
|
||||||
|
def process_log_line(line, validate_json=False, output_file=None):
|
||||||
|
"""Process a single log line."""
|
||||||
|
line = line.strip()
|
||||||
|
if not line:
|
||||||
|
return
|
||||||
|
|
||||||
|
if validate_json:
|
||||||
|
try:
|
||||||
|
log_entry = json.loads(line)
|
||||||
|
line = json.dumps(log_entry, indent=2)
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
line = f"[INVALID JSON] {line}\nError: {e}"
|
||||||
|
|
||||||
|
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
output = f"[{timestamp}] {line}"
|
||||||
|
|
||||||
|
if output_file:
|
||||||
|
output_file.write(output + "\n")
|
||||||
|
output_file.flush()
|
||||||
|
else:
|
||||||
|
print(output)
|
||||||
|
|
||||||
|
|
||||||
|
def handle_client(conn, validate_json=False, output_file=None):
|
||||||
|
"""Handle a client connection."""
|
||||||
|
data = b""
|
||||||
|
try:
|
||||||
|
while not shutdown_requested:
|
||||||
|
chunk = conn.recv(4096)
|
||||||
|
if not chunk:
|
||||||
|
break
|
||||||
|
|
||||||
|
data += chunk
|
||||||
|
|
||||||
|
# Process complete lines
|
||||||
|
while b"\n" in data:
|
||||||
|
newline_pos = data.index(b"\n")
|
||||||
|
line = data[:newline_pos].decode("utf-8", errors="replace")
|
||||||
|
data = data[newline_pos + 1:]
|
||||||
|
process_log_line(line, validate_json, output_file)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error handling client: {e}", file=sys.stderr)
|
||||||
|
finally:
|
||||||
|
# Process any remaining data
|
||||||
|
if data:
|
||||||
|
line = data.decode("utf-8", errors="replace")
|
||||||
|
process_log_line(line, validate_json, output_file)
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Main entry point."""
|
||||||
|
args = parse_args()
|
||||||
|
|
||||||
|
# Setup signal handlers
|
||||||
|
signal.signal(signal.SIGINT, signal_handler)
|
||||||
|
signal.signal(signal.SIGTERM, signal_handler)
|
||||||
|
|
||||||
|
output_file = None
|
||||||
|
if args.output:
|
||||||
|
output_file = open(args.output, "a")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Create socket
|
||||||
|
server = create_socket(args.socket_path)
|
||||||
|
print(f"Listening on {args.socket_path}", file=sys.stderr)
|
||||||
|
if not args.quiet:
|
||||||
|
print(f"Waiting for connections... (Ctrl+C to stop)", file=sys.stderr)
|
||||||
|
|
||||||
|
# Accept connections
|
||||||
|
while not shutdown_requested:
|
||||||
|
try:
|
||||||
|
server.settimeout(1.0)
|
||||||
|
try:
|
||||||
|
conn, addr = server.accept()
|
||||||
|
except socket.timeout:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Handle client in same thread for simplicity
|
||||||
|
handle_client(conn, args.validate_json, output_file)
|
||||||
|
except Exception as e:
|
||||||
|
if not shutdown_requested:
|
||||||
|
print(f"Accept error: {e}", file=sys.stderr)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Fatal error: {e}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
finally:
|
||||||
|
# Cleanup
|
||||||
|
if os.path.exists(args.socket_path):
|
||||||
|
os.remove(args.socket_path)
|
||||||
|
if output_file:
|
||||||
|
output_file.close()
|
||||||
|
print("Socket consumer stopped.", file=sys.stderr)
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
76
scripts/socket_listener.py
Normal file
76
scripts/socket_listener.py
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
socket_listener.py - Simple Unix socket listener for testing mod_reqin_log
|
||||||
|
Receives JSON log lines and writes them to a file.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import socket
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import signal
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
shutdown_requested = False
|
||||||
|
|
||||||
|
def signal_handler(signum, frame):
|
||||||
|
global shutdown_requested
|
||||||
|
shutdown_requested = True
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description='Unix socket listener for testing')
|
||||||
|
parser.add_argument('socket_path', help='Path to Unix socket')
|
||||||
|
parser.add_argument('-o', '--output', required=True, help='Output file for logs')
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
signal.signal(signal.SIGINT, signal_handler)
|
||||||
|
signal.signal(signal.SIGTERM, signal_handler)
|
||||||
|
|
||||||
|
# Remove existing socket
|
||||||
|
if os.path.exists(args.socket_path):
|
||||||
|
os.remove(args.socket_path)
|
||||||
|
|
||||||
|
# Create socket
|
||||||
|
server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||||
|
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
|
server.bind(args.socket_path)
|
||||||
|
server.listen(5)
|
||||||
|
os.chmod(args.socket_path, 0o666)
|
||||||
|
|
||||||
|
print(f"Listening on {args.socket_path}", file=sys.stderr)
|
||||||
|
sys.stderr.flush()
|
||||||
|
|
||||||
|
with open(args.output, 'w') as f:
|
||||||
|
while not shutdown_requested:
|
||||||
|
try:
|
||||||
|
server.settimeout(1.0)
|
||||||
|
try:
|
||||||
|
conn, addr = server.accept()
|
||||||
|
print(f"Connection accepted from {addr}", file=sys.stderr)
|
||||||
|
sys.stderr.flush()
|
||||||
|
except socket.timeout:
|
||||||
|
continue
|
||||||
|
|
||||||
|
data = b""
|
||||||
|
while not shutdown_requested:
|
||||||
|
chunk = conn.recv(4096)
|
||||||
|
if not chunk:
|
||||||
|
break
|
||||||
|
data += chunk
|
||||||
|
while b"\n" in data:
|
||||||
|
newline_pos = data.index(b"\n")
|
||||||
|
line = data[:newline_pos].decode("utf-8", errors="replace")
|
||||||
|
data = data[newline_pos + 1:]
|
||||||
|
if line.strip():
|
||||||
|
f.write(line + "\n")
|
||||||
|
f.flush()
|
||||||
|
print(f"Received: {line[:100]}...", file=sys.stderr)
|
||||||
|
conn.close()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error: {e}", file=sys.stderr)
|
||||||
|
|
||||||
|
if os.path.exists(args.socket_path):
|
||||||
|
os.remove(args.socket_path)
|
||||||
|
print("Listener stopped.", file=sys.stderr)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
31
scripts/test.sh
Executable file
31
scripts/test.sh
Executable file
@ -0,0 +1,31 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# test.sh - Run tests for mod_reqin_log in Docker
|
||||||
|
#
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
IMAGE_NAME="mod_reqin_log-build"
|
||||||
|
|
||||||
|
echo "========================================"
|
||||||
|
echo "mod_reqin_log - Test Suite"
|
||||||
|
echo "========================================"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Build image if not exists
|
||||||
|
if ! docker images "$IMAGE_NAME" | grep -q "$IMAGE_NAME"; then
|
||||||
|
echo "Building Docker image first..."
|
||||||
|
"$SCRIPT_DIR/scripts/build.sh"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Running unit tests..."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Run unit tests in container
|
||||||
|
docker run --rm "$IMAGE_NAME" bash -c "cd build/tests && ctest --output-on-failure"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "========================================"
|
||||||
|
echo "Unit tests completed"
|
||||||
|
echo "========================================"
|
||||||
317
scripts/test_unix_socket.sh
Executable file
317
scripts/test_unix_socket.sh
Executable file
@ -0,0 +1,317 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# test_unix_socket.sh - Integration test for mod_reqin_log Unix socket logging
|
||||||
|
#
|
||||||
|
# This test verifies that:
|
||||||
|
# 1. The module connects to a Unix socket
|
||||||
|
# 2. HTTP requests generate JSON log entries
|
||||||
|
# 3. Log entries are properly formatted and sent to the socket
|
||||||
|
#
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
SOCKET_PATH="/tmp/mod_reqin_log_test.sock"
|
||||||
|
LOG_OUTPUT="/tmp/mod_reqin_log_output.jsonl"
|
||||||
|
APACHE_PORT="${APACHE_PORT:-8080}"
|
||||||
|
TIMEOUT=30
|
||||||
|
|
||||||
|
# Colors
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
log_info() {
|
||||||
|
echo -e "${YELLOW}[INFO]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
log_pass() {
|
||||||
|
echo -e "${GREEN}[PASS]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
log_fail() {
|
||||||
|
echo -e "${RED}[FAIL]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
log_info "Cleaning up..."
|
||||||
|
rm -f "$SOCKET_PATH" "$LOG_OUTPUT"
|
||||||
|
pkill -f "socket_listener.py" 2>/dev/null || true
|
||||||
|
pkill -f "apache.*test" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
# Check dependencies
|
||||||
|
check_dependencies() {
|
||||||
|
log_info "Checking dependencies..."
|
||||||
|
if ! command -v curl &> /dev/null; then
|
||||||
|
log_fail "curl is required but not installed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if ! command -v python3 &> /dev/null; then
|
||||||
|
log_fail "python3 is required but not installed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
log_pass "Dependencies OK"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Start Unix socket listener that logs received data
|
||||||
|
start_socket_listener() {
|
||||||
|
log_info "Starting Unix socket listener on $SOCKET_PATH..."
|
||||||
|
rm -f "$SOCKET_PATH"
|
||||||
|
|
||||||
|
# Use Python script to listen on Unix socket
|
||||||
|
python3 /build/scripts/socket_listener.py "$SOCKET_PATH" -o "$LOG_OUTPUT" &
|
||||||
|
LISTENER_PID=$!
|
||||||
|
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
if ! kill -0 $LISTENER_PID 2>/dev/null; then
|
||||||
|
log_fail "Failed to start socket listener"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_pass "Socket listener started (PID: $LISTENER_PID)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create Apache test configuration
|
||||||
|
create_apache_config() {
|
||||||
|
log_info "Creating Apache test configuration..."
|
||||||
|
|
||||||
|
cat > /tmp/httpd_test.conf << EOF
|
||||||
|
ServerRoot "/etc/httpd"
|
||||||
|
Listen $APACHE_PORT
|
||||||
|
ServerName localhost
|
||||||
|
|
||||||
|
LoadModule reqin_log_module /build/.libs/mod_reqin_log.so
|
||||||
|
LoadModule mpm_event_module modules/mod_mpm_event.so
|
||||||
|
LoadModule authz_core_module modules/mod_authz_core.so
|
||||||
|
LoadModule dir_module modules/mod_dir.so
|
||||||
|
LoadModule mime_module modules/mod_mime.so
|
||||||
|
LoadModule unixd_module modules/mod_unixd.so
|
||||||
|
LoadModule log_config_module modules/mod_log_config.so
|
||||||
|
|
||||||
|
User apache
|
||||||
|
Group apache
|
||||||
|
|
||||||
|
TypesConfig /etc/mime.types
|
||||||
|
DirectoryIndex index.html
|
||||||
|
|
||||||
|
JsonSockLogEnabled On
|
||||||
|
JsonSockLogSocket "$SOCKET_PATH"
|
||||||
|
JsonSockLogHeaders X-Request-Id User-Agent X-Test-Header
|
||||||
|
JsonSockLogMaxHeaders 10
|
||||||
|
JsonSockLogMaxHeaderValueLen 256
|
||||||
|
JsonSockLogReconnectInterval 5
|
||||||
|
JsonSockLogErrorReportInterval 5
|
||||||
|
|
||||||
|
<VirtualHost *:$APACHE_PORT>
|
||||||
|
DocumentRoot /var/www/html
|
||||||
|
<Directory /var/www/html>
|
||||||
|
Require all granted
|
||||||
|
</Directory>
|
||||||
|
</VirtualHost>
|
||||||
|
|
||||||
|
ErrorLog /dev/stderr
|
||||||
|
LogLevel warn
|
||||||
|
EOF
|
||||||
|
|
||||||
|
log_pass "Apache configuration created"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Start Apache with test configuration
|
||||||
|
start_apache() {
|
||||||
|
log_info "Starting Apache with mod_reqin_log..."
|
||||||
|
|
||||||
|
# Create document root if needed
|
||||||
|
mkdir -p /var/www/html
|
||||||
|
echo "<html><body><h1>Test</h1></body></html>" > /var/www/html/index.html
|
||||||
|
|
||||||
|
# Check socket exists
|
||||||
|
if [ ! -S "$SOCKET_PATH" ]; then
|
||||||
|
log_fail "Socket file does not exist: $SOCKET_PATH"
|
||||||
|
ls -la /tmp/*.sock 2>&1 || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
log_info "Socket file exists: $SOCKET_PATH"
|
||||||
|
ls -la "$SOCKET_PATH"
|
||||||
|
|
||||||
|
# Start Apache and capture stderr
|
||||||
|
httpd -f /tmp/httpd_test.conf -DFOREGROUND 2>&1 &
|
||||||
|
APACHE_PID=$!
|
||||||
|
|
||||||
|
# Wait for Apache to start
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
if ! kill -0 $APACHE_PID 2>/dev/null; then
|
||||||
|
log_fail "Failed to start Apache"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_pass "Apache started (PID: $APACHE_PID)"
|
||||||
|
|
||||||
|
# Wait for Apache workers to initialize and connect to socket
|
||||||
|
log_info "Waiting for Apache workers to initialize..."
|
||||||
|
sleep 3
|
||||||
|
}
|
||||||
|
|
||||||
|
# Send test HTTP requests
|
||||||
|
send_test_requests() {
|
||||||
|
log_info "Sending test HTTP requests..."
|
||||||
|
|
||||||
|
# Health check first
|
||||||
|
local retries=5
|
||||||
|
while [ $retries -gt 0 ]; do
|
||||||
|
if curl -s -o /dev/null -w "%{http_code}" "http://localhost:$APACHE_PORT/" | grep -q "200"; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
retries=$((retries - 1))
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ $retries -eq 0 ]; then
|
||||||
|
log_fail "Apache health check failed"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
log_info "Apache health check passed"
|
||||||
|
|
||||||
|
# Request 1: Basic GET
|
||||||
|
curl -s "http://localhost:$APACHE_PORT/" > /dev/null
|
||||||
|
sleep 0.5
|
||||||
|
|
||||||
|
# Request 2: With custom headers
|
||||||
|
curl -s -H "X-Request-Id: test-12345" "http://localhost:$APACHE_PORT/" > /dev/null
|
||||||
|
sleep 0.5
|
||||||
|
|
||||||
|
# Request 3: POST request
|
||||||
|
curl -s -X POST -d "test=data" "http://localhost:$APACHE_PORT/" > /dev/null
|
||||||
|
sleep 0.5
|
||||||
|
|
||||||
|
# Request 4: With User-Agent
|
||||||
|
curl -s -A "TestAgent/1.0" "http://localhost:$APACHE_PORT/api/test" > /dev/null
|
||||||
|
sleep 0.5
|
||||||
|
|
||||||
|
# Request 5: Multiple headers
|
||||||
|
curl -s \
|
||||||
|
-H "X-Request-Id: req-abc" \
|
||||||
|
-H "X-Test-Header: header-value" \
|
||||||
|
-H "User-Agent: Mozilla/5.0" \
|
||||||
|
"http://localhost:$APACHE_PORT/page" > /dev/null
|
||||||
|
sleep 1
|
||||||
|
|
||||||
|
log_pass "Test requests sent"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Verify log output
|
||||||
|
verify_logs() {
|
||||||
|
log_info "Verifying log output..."
|
||||||
|
|
||||||
|
if [ ! -f "$LOG_OUTPUT" ]; then
|
||||||
|
log_fail "Log file not created"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local log_count=$(wc -l < "$LOG_OUTPUT")
|
||||||
|
if [ "$log_count" -lt 1 ]; then
|
||||||
|
log_fail "Expected at least 1 log entry, got $log_count"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_pass "Received $log_count log entries"
|
||||||
|
|
||||||
|
# Show all log entries
|
||||||
|
log_info "Log file contents:"
|
||||||
|
cat "$LOG_OUTPUT"
|
||||||
|
|
||||||
|
# Verify JSON format
|
||||||
|
log_info "Validating JSON format..."
|
||||||
|
local invalid_json=0
|
||||||
|
while IFS= read -r line; do
|
||||||
|
if [ -n "$line" ]; then
|
||||||
|
if ! echo "$line" | python3 -m json.tool > /dev/null 2>&1; then
|
||||||
|
log_fail "Invalid JSON: $line"
|
||||||
|
invalid_json=1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done < "$LOG_OUTPUT"
|
||||||
|
|
||||||
|
if [ $invalid_json -eq 0 ]; then
|
||||||
|
log_pass "All JSON entries are valid"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verify required fields
|
||||||
|
log_info "Checking required fields..."
|
||||||
|
local first_line=$(head -1 "$LOG_OUTPUT")
|
||||||
|
|
||||||
|
local required_fields=("time" "timestamp" "src_ip" "dst_ip" "method" "path" "host")
|
||||||
|
local missing_fields=0
|
||||||
|
|
||||||
|
for field in "${required_fields[@]}"; do
|
||||||
|
if ! echo "$first_line" | grep -q "\"$field\":"; then
|
||||||
|
log_fail "Missing required field: $field"
|
||||||
|
missing_fields=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ $missing_fields -eq 0 ]; then
|
||||||
|
log_pass "All required fields present"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verify header logging
|
||||||
|
log_info "Checking header logging..."
|
||||||
|
if grep -q '"header_X-Request-Id"' "$LOG_OUTPUT"; then
|
||||||
|
log_pass "Custom headers are logged"
|
||||||
|
else
|
||||||
|
log_info "Custom headers test skipped (no X-Request-Id in requests)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verify HTTP methods
|
||||||
|
log_info "Checking HTTP methods..."
|
||||||
|
if grep -q '"method":"GET"' "$LOG_OUTPUT"; then
|
||||||
|
log_pass "GET method logged"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if grep -q '"method":"POST"' "$LOG_OUTPUT"; then
|
||||||
|
log_pass "POST method logged"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Show sample log entry
|
||||||
|
log_info "Sample log entry (formatted):"
|
||||||
|
head -1 "$LOG_OUTPUT" | python3 -m json.tool 2>/dev/null || head -1 "$LOG_OUTPUT"
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main test runner
|
||||||
|
main() {
|
||||||
|
echo "========================================"
|
||||||
|
echo "mod_reqin_log Unix Socket Integration Test"
|
||||||
|
echo "========================================"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
check_dependencies
|
||||||
|
start_socket_listener
|
||||||
|
create_apache_config
|
||||||
|
start_apache
|
||||||
|
send_test_requests
|
||||||
|
|
||||||
|
log_info "Waiting for logs to be written..."
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
verify_logs
|
||||||
|
local result=$?
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "========================================"
|
||||||
|
if [ $result -eq 0 ]; then
|
||||||
|
echo -e "${GREEN}All tests passed!${NC}"
|
||||||
|
else
|
||||||
|
echo -e "${RED}Some tests failed!${NC}"
|
||||||
|
fi
|
||||||
|
echo "========================================"
|
||||||
|
|
||||||
|
return $result
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
614
src/mod_reqin_log.c
Normal file
614
src/mod_reqin_log.c
Normal file
@ -0,0 +1,614 @@
|
|||||||
|
/*
|
||||||
|
* mod_reqin_log.c - Apache HTTPD module for logging HTTP requests as JSON to Unix socket
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "httpd.h"
|
||||||
|
#include "http_config.h"
|
||||||
|
#include "http_core.h"
|
||||||
|
#include "http_log.h"
|
||||||
|
#include "http_protocol.h"
|
||||||
|
#include "http_request.h"
|
||||||
|
#include "apr_strings.h"
|
||||||
|
#include "apr_time.h"
|
||||||
|
#include "apr_lib.h"
|
||||||
|
#include "ap_config.h"
|
||||||
|
|
||||||
|
#include <sys/socket.h>
|
||||||
|
#include <sys/un.h>
|
||||||
|
#include <netinet/in.h>
|
||||||
|
#include <arpa/inet.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <time.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#define MOD_REQIN_LOG_NAME "mod_reqin_log"
|
||||||
|
|
||||||
|
/* 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
|
||||||
|
|
||||||
|
/* 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;
|
||||||
|
|
||||||
|
/* Dynamic string buffer */
|
||||||
|
typedef struct {
|
||||||
|
char *data;
|
||||||
|
apr_size_t len;
|
||||||
|
apr_size_t capacity;
|
||||||
|
apr_pool_t *pool;
|
||||||
|
} dynbuf_t;
|
||||||
|
|
||||||
|
/* Per-child process state */
|
||||||
|
typedef struct {
|
||||||
|
int socket_fd;
|
||||||
|
apr_time_t last_connect_attempt;
|
||||||
|
apr_time_t last_error_report;
|
||||||
|
int connect_failed;
|
||||||
|
} reqin_log_child_state_t;
|
||||||
|
|
||||||
|
/* Global child state (one per process) */
|
||||||
|
static reqin_log_child_state_t g_child_state = {
|
||||||
|
.socket_fd = -1,
|
||||||
|
.last_connect_attempt = 0,
|
||||||
|
.last_error_report = 0,
|
||||||
|
.connect_failed = 0
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Forward declarations for helper functions */
|
||||||
|
static void dynbuf_append(dynbuf_t *db, const char *str, apr_size_t len);
|
||||||
|
static void append_json_string(dynbuf_t *db, const char *str);
|
||||||
|
static void format_iso8601(dynbuf_t *db, apr_time_t t);
|
||||||
|
|
||||||
|
/* Forward declarations for commands */
|
||||||
|
static const char *cmd_set_enabled(cmd_parms *cmd, void *cfg, int flag);
|
||||||
|
static const char *cmd_set_socket(cmd_parms *cmd, void *cfg, const char *arg);
|
||||||
|
static const char *cmd_set_headers(cmd_parms *cmd, void *cfg, const char *arg);
|
||||||
|
static const char *cmd_set_max_headers(cmd_parms *cmd, void *cfg, const char *arg);
|
||||||
|
static const char *cmd_set_max_header_value_len(cmd_parms *cmd, void *cfg, const char *arg);
|
||||||
|
static const char *cmd_set_reconnect_interval(cmd_parms *cmd, void *cfg, const char *arg);
|
||||||
|
static const char *cmd_set_error_report_interval(cmd_parms *cmd, void *cfg, const char *arg);
|
||||||
|
|
||||||
|
/* Forward declarations for hooks */
|
||||||
|
static int reqin_log_post_read_request(request_rec *r);
|
||||||
|
static void reqin_log_child_init(apr_pool_t *p, server_rec *s);
|
||||||
|
static void reqin_log_register_hooks(apr_pool_t *p);
|
||||||
|
|
||||||
|
/* Command table */
|
||||||
|
static const command_rec reqin_log_cmds[] = {
|
||||||
|
AP_INIT_FLAG("JsonSockLogEnabled", cmd_set_enabled, NULL, RSRC_CONF,
|
||||||
|
"Enable or disable mod_reqin_log (On|Off)"),
|
||||||
|
AP_INIT_TAKE1("JsonSockLogSocket", cmd_set_socket, NULL, RSRC_CONF,
|
||||||
|
"Unix domain socket path for JSON logging"),
|
||||||
|
AP_INIT_ITERATE("JsonSockLogHeaders", cmd_set_headers, NULL, RSRC_CONF,
|
||||||
|
"List of HTTP header names to log"),
|
||||||
|
AP_INIT_TAKE1("JsonSockLogMaxHeaders", cmd_set_max_headers, NULL, RSRC_CONF,
|
||||||
|
"Maximum number of headers to log (default: 10)"),
|
||||||
|
AP_INIT_TAKE1("JsonSockLogMaxHeaderValueLen", cmd_set_max_header_value_len, NULL, RSRC_CONF,
|
||||||
|
"Maximum length of header value to log (default: 256)"),
|
||||||
|
AP_INIT_TAKE1("JsonSockLogReconnectInterval", cmd_set_reconnect_interval, NULL, RSRC_CONF,
|
||||||
|
"Reconnect interval in seconds (default: 10)"),
|
||||||
|
AP_INIT_TAKE1("JsonSockLogErrorReportInterval", cmd_set_error_report_interval, NULL, RSRC_CONF,
|
||||||
|
"Error report interval in seconds (default: 10)"),
|
||||||
|
{ NULL }
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Module definition */
|
||||||
|
module AP_MODULE_DECLARE_DATA reqin_log_module = {
|
||||||
|
STANDARD20_MODULE_STUFF,
|
||||||
|
NULL, /* per-directory config creator */
|
||||||
|
NULL, /* dir config merger */
|
||||||
|
NULL, /* server config creator */
|
||||||
|
NULL, /* server config merger */
|
||||||
|
reqin_log_cmds, /* command table */
|
||||||
|
reqin_log_register_hooks /* register hooks */
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Get module configuration */
|
||||||
|
static reqin_log_config_t *get_module_config(server_rec *s)
|
||||||
|
{
|
||||||
|
reqin_log_config_t *cfg = (reqin_log_config_t *)ap_get_module_config(s->module_config, &reqin_log_module);
|
||||||
|
return cfg;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============== Dynamic Buffer Functions ============== */
|
||||||
|
|
||||||
|
static void dynbuf_init(dynbuf_t *db, apr_pool_t *pool, apr_size_t initial_capacity)
|
||||||
|
{
|
||||||
|
db->pool = pool;
|
||||||
|
db->capacity = initial_capacity;
|
||||||
|
db->len = 0;
|
||||||
|
db->data = apr_palloc(pool, initial_capacity);
|
||||||
|
db->data[0] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
static void dynbuf_append(dynbuf_t *db, const char *str, apr_size_t len)
|
||||||
|
{
|
||||||
|
if (str == NULL) return;
|
||||||
|
|
||||||
|
if (len == (apr_size_t)-1) {
|
||||||
|
len = strlen(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (db->len + len >= db->capacity) {
|
||||||
|
apr_size_t new_capacity = (db->len + len + 1) * 2;
|
||||||
|
char *new_data = apr_palloc(db->pool, new_capacity);
|
||||||
|
memcpy(new_data, db->data, db->len);
|
||||||
|
db->data = new_data;
|
||||||
|
db->capacity = new_capacity;
|
||||||
|
}
|
||||||
|
|
||||||
|
memcpy(db->data + db->len, str, len);
|
||||||
|
db->len += len;
|
||||||
|
db->data[db->len] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
static void dynbuf_append_char(dynbuf_t *db, char c)
|
||||||
|
{
|
||||||
|
if (db->len + 1 >= db->capacity) {
|
||||||
|
apr_size_t new_capacity = db->capacity * 2;
|
||||||
|
char *new_data = apr_palloc(db->pool, new_capacity);
|
||||||
|
memcpy(new_data, db->data, db->len);
|
||||||
|
db->data = new_data;
|
||||||
|
db->capacity = new_capacity;
|
||||||
|
}
|
||||||
|
db->data[db->len++] = c;
|
||||||
|
db->data[db->len] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============== JSON Helper Functions ============== */
|
||||||
|
|
||||||
|
static void append_json_string(dynbuf_t *db, const char *str)
|
||||||
|
{
|
||||||
|
if (str == NULL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const char *p = str; *p; p++) {
|
||||||
|
char c = *p;
|
||||||
|
switch (c) {
|
||||||
|
case '"': dynbuf_append(db, "\\\"", 2); break;
|
||||||
|
case '\\': dynbuf_append(db, "\\\\", 2); break;
|
||||||
|
case '\b': dynbuf_append(db, "\\b", 2); break;
|
||||||
|
case '\f': dynbuf_append(db, "\\f", 2); break;
|
||||||
|
case '\n': dynbuf_append(db, "\\n", 2); break;
|
||||||
|
case '\r': dynbuf_append(db, "\\r", 2); break;
|
||||||
|
case '\t': dynbuf_append(db, "\\t", 2); break;
|
||||||
|
default:
|
||||||
|
if ((unsigned char)c < 0x20) {
|
||||||
|
char unicode[8];
|
||||||
|
snprintf(unicode, sizeof(unicode), "\\u%04x", (unsigned char)c);
|
||||||
|
dynbuf_append(db, unicode, -1);
|
||||||
|
} else {
|
||||||
|
dynbuf_append_char(db, c);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void format_iso8601(dynbuf_t *db, apr_time_t t)
|
||||||
|
{
|
||||||
|
apr_time_exp_t tm;
|
||||||
|
apr_time_exp_gmt(&tm, t);
|
||||||
|
|
||||||
|
char time_str[32];
|
||||||
|
snprintf(time_str, sizeof(time_str), "%04d-%02d-%02dT%02d:%02d:%02dZ",
|
||||||
|
tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday,
|
||||||
|
tm.tm_hour, tm.tm_min, tm.tm_sec);
|
||||||
|
dynbuf_append(db, time_str, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============== Configuration Command Handlers ============== */
|
||||||
|
|
||||||
|
static const char *cmd_set_enabled(cmd_parms *cmd, void *cfg, int flag)
|
||||||
|
{
|
||||||
|
reqin_log_config_t *conf = get_module_config(cmd->server);
|
||||||
|
if (conf == NULL) {
|
||||||
|
conf = apr_pcalloc(cmd->pool, sizeof(reqin_log_config_t));
|
||||||
|
conf->headers = apr_array_make(cmd->pool, 0, sizeof(const char *));
|
||||||
|
conf->max_headers = DEFAULT_MAX_HEADERS;
|
||||||
|
conf->max_header_value_len = DEFAULT_MAX_HEADER_VALUE_LEN;
|
||||||
|
conf->reconnect_interval = DEFAULT_RECONNECT_INTERVAL;
|
||||||
|
conf->error_report_interval = DEFAULT_ERROR_REPORT_INTERVAL;
|
||||||
|
ap_set_module_config(cmd->server->module_config, &reqin_log_module, conf);
|
||||||
|
}
|
||||||
|
conf->enabled = flag ? 1 : 0;
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
static const char *cmd_set_socket(cmd_parms *cmd, void *cfg, const char *arg)
|
||||||
|
{
|
||||||
|
reqin_log_config_t *conf = get_module_config(cmd->server);
|
||||||
|
if (conf == NULL) {
|
||||||
|
conf = apr_pcalloc(cmd->pool, sizeof(reqin_log_config_t));
|
||||||
|
conf->enabled = 0;
|
||||||
|
conf->headers = apr_array_make(cmd->pool, 0, sizeof(const char *));
|
||||||
|
conf->max_headers = DEFAULT_MAX_HEADERS;
|
||||||
|
conf->max_header_value_len = DEFAULT_MAX_HEADER_VALUE_LEN;
|
||||||
|
conf->reconnect_interval = DEFAULT_RECONNECT_INTERVAL;
|
||||||
|
conf->error_report_interval = DEFAULT_ERROR_REPORT_INTERVAL;
|
||||||
|
ap_set_module_config(cmd->server->module_config, &reqin_log_module, conf);
|
||||||
|
}
|
||||||
|
conf->socket_path = apr_pstrdup(cmd->pool, arg);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
static const char *cmd_set_headers(cmd_parms *cmd, void *cfg, const char *arg)
|
||||||
|
{
|
||||||
|
reqin_log_config_t *conf = get_module_config(cmd->server);
|
||||||
|
if (conf == NULL) {
|
||||||
|
conf = apr_pcalloc(cmd->pool, sizeof(reqin_log_config_t));
|
||||||
|
conf->enabled = 0;
|
||||||
|
conf->socket_path = NULL;
|
||||||
|
conf->max_headers = DEFAULT_MAX_HEADERS;
|
||||||
|
conf->max_header_value_len = DEFAULT_MAX_HEADER_VALUE_LEN;
|
||||||
|
conf->reconnect_interval = DEFAULT_RECONNECT_INTERVAL;
|
||||||
|
conf->error_report_interval = DEFAULT_ERROR_REPORT_INTERVAL;
|
||||||
|
ap_set_module_config(cmd->server->module_config, &reqin_log_module, conf);
|
||||||
|
}
|
||||||
|
if (conf->headers == NULL) {
|
||||||
|
conf->headers = apr_array_make(cmd->pool, 5, sizeof(const char *));
|
||||||
|
}
|
||||||
|
*(const char **)apr_array_push(conf->headers) = apr_pstrdup(cmd->pool, arg);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
static const char *cmd_set_max_headers(cmd_parms *cmd, void *cfg, const char *arg)
|
||||||
|
{
|
||||||
|
reqin_log_config_t *conf = get_module_config(cmd->server);
|
||||||
|
if (conf == NULL) {
|
||||||
|
conf = apr_pcalloc(cmd->pool, sizeof(reqin_log_config_t));
|
||||||
|
conf->enabled = 0;
|
||||||
|
conf->socket_path = NULL;
|
||||||
|
conf->headers = apr_array_make(cmd->pool, 0, sizeof(const char *));
|
||||||
|
conf->max_header_value_len = DEFAULT_MAX_HEADER_VALUE_LEN;
|
||||||
|
conf->reconnect_interval = DEFAULT_RECONNECT_INTERVAL;
|
||||||
|
conf->error_report_interval = DEFAULT_ERROR_REPORT_INTERVAL;
|
||||||
|
ap_set_module_config(cmd->server->module_config, &reqin_log_module, conf);
|
||||||
|
}
|
||||||
|
int val = atoi(arg);
|
||||||
|
if (val < 0) {
|
||||||
|
return "JsonSockLogMaxHeaders must be >= 0";
|
||||||
|
}
|
||||||
|
conf->max_headers = val;
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
static const char *cmd_set_max_header_value_len(cmd_parms *cmd, void *cfg, const char *arg)
|
||||||
|
{
|
||||||
|
reqin_log_config_t *conf = get_module_config(cmd->server);
|
||||||
|
if (conf == NULL) {
|
||||||
|
conf = apr_pcalloc(cmd->pool, sizeof(reqin_log_config_t));
|
||||||
|
conf->enabled = 0;
|
||||||
|
conf->socket_path = NULL;
|
||||||
|
conf->headers = apr_array_make(cmd->pool, 0, sizeof(const char *));
|
||||||
|
conf->max_headers = DEFAULT_MAX_HEADERS;
|
||||||
|
conf->reconnect_interval = DEFAULT_RECONNECT_INTERVAL;
|
||||||
|
conf->error_report_interval = DEFAULT_ERROR_REPORT_INTERVAL;
|
||||||
|
ap_set_module_config(cmd->server->module_config, &reqin_log_module, conf);
|
||||||
|
}
|
||||||
|
int val = atoi(arg);
|
||||||
|
if (val < 1) {
|
||||||
|
return "JsonSockLogMaxHeaderValueLen must be >= 1";
|
||||||
|
}
|
||||||
|
conf->max_header_value_len = val;
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
static const char *cmd_set_reconnect_interval(cmd_parms *cmd, void *cfg, const char *arg)
|
||||||
|
{
|
||||||
|
reqin_log_config_t *conf = get_module_config(cmd->server);
|
||||||
|
if (conf == NULL) {
|
||||||
|
conf = apr_pcalloc(cmd->pool, sizeof(reqin_log_config_t));
|
||||||
|
conf->enabled = 0;
|
||||||
|
conf->socket_path = NULL;
|
||||||
|
conf->headers = apr_array_make(cmd->pool, 0, sizeof(const char *));
|
||||||
|
conf->max_headers = DEFAULT_MAX_HEADERS;
|
||||||
|
conf->max_header_value_len = DEFAULT_MAX_HEADER_VALUE_LEN;
|
||||||
|
conf->error_report_interval = DEFAULT_ERROR_REPORT_INTERVAL;
|
||||||
|
ap_set_module_config(cmd->server->module_config, &reqin_log_module, conf);
|
||||||
|
}
|
||||||
|
int val = atoi(arg);
|
||||||
|
if (val < 0) {
|
||||||
|
return "JsonSockLogReconnectInterval must be >= 0";
|
||||||
|
}
|
||||||
|
conf->reconnect_interval = val;
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
static const char *cmd_set_error_report_interval(cmd_parms *cmd, void *cfg, const char *arg)
|
||||||
|
{
|
||||||
|
reqin_log_config_t *conf = get_module_config(cmd->server);
|
||||||
|
if (conf == NULL) {
|
||||||
|
conf = apr_pcalloc(cmd->pool, sizeof(reqin_log_config_t));
|
||||||
|
conf->enabled = 0;
|
||||||
|
conf->socket_path = NULL;
|
||||||
|
conf->headers = apr_array_make(cmd->pool, 0, sizeof(const char *));
|
||||||
|
conf->max_headers = DEFAULT_MAX_HEADERS;
|
||||||
|
conf->max_header_value_len = DEFAULT_MAX_HEADER_VALUE_LEN;
|
||||||
|
conf->reconnect_interval = DEFAULT_RECONNECT_INTERVAL;
|
||||||
|
ap_set_module_config(cmd->server->module_config, &reqin_log_module, conf);
|
||||||
|
}
|
||||||
|
int val = atoi(arg);
|
||||||
|
if (val < 0) {
|
||||||
|
return "JsonSockLogErrorReportInterval must be >= 0";
|
||||||
|
}
|
||||||
|
conf->error_report_interval = val;
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============== Socket Functions ============== */
|
||||||
|
|
||||||
|
static int try_connect(reqin_log_config_t *cfg, server_rec *s)
|
||||||
|
{
|
||||||
|
apr_time_t now = apr_time_now();
|
||||||
|
apr_time_t interval = apr_time_from_sec(cfg->reconnect_interval);
|
||||||
|
|
||||||
|
if (g_child_state.connect_failed &&
|
||||||
|
(now - g_child_state.last_connect_attempt) < interval) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
g_child_state.last_connect_attempt = now;
|
||||||
|
|
||||||
|
if (g_child_state.socket_fd < 0) {
|
||||||
|
g_child_state.socket_fd = socket(AF_UNIX, SOCK_STREAM, 0);
|
||||||
|
if (g_child_state.socket_fd < 0) {
|
||||||
|
ap_log_error(APLOG_MARK, APLOG_ERR, errno, s,
|
||||||
|
MOD_REQIN_LOG_NAME ": Failed to create socket");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int flags = fcntl(g_child_state.socket_fd, F_GETFL, 0);
|
||||||
|
fcntl(g_child_state.socket_fd, F_SETFL, flags | O_NONBLOCK);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct sockaddr_un addr;
|
||||||
|
memset(&addr, 0, sizeof(addr));
|
||||||
|
addr.sun_family = AF_UNIX;
|
||||||
|
snprintf(addr.sun_path, sizeof(addr.sun_path), "%s", cfg->socket_path);
|
||||||
|
|
||||||
|
int rc = connect(g_child_state.socket_fd, (struct sockaddr *)&addr, sizeof(addr));
|
||||||
|
if (rc < 0) {
|
||||||
|
int err = errno;
|
||||||
|
if (err != EINPROGRESS && err != EAGAIN && err != EWOULDBLOCK) {
|
||||||
|
close(g_child_state.socket_fd);
|
||||||
|
g_child_state.socket_fd = -1;
|
||||||
|
g_child_state.connect_failed = 1;
|
||||||
|
|
||||||
|
if ((now - g_child_state.last_error_report) >= apr_time_from_sec(cfg->error_report_interval)) {
|
||||||
|
ap_log_error(APLOG_MARK, APLOG_ERR, err, s,
|
||||||
|
MOD_REQIN_LOG_NAME ": Unix socket connect failed: %s", cfg->socket_path);
|
||||||
|
g_child_state.last_error_report = now;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
g_child_state.connect_failed = 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int ensure_connected(reqin_log_config_t *cfg, server_rec *s)
|
||||||
|
{
|
||||||
|
if (g_child_state.socket_fd >= 0 && !g_child_state.connect_failed) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return try_connect(cfg, s);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int write_to_socket(const char *data, apr_size_t len, server_rec *s, reqin_log_config_t *cfg)
|
||||||
|
{
|
||||||
|
if (g_child_state.socket_fd < 0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
apr_size_t total_written = 0;
|
||||||
|
while (total_written < len) {
|
||||||
|
ssize_t n = write(g_child_state.socket_fd, data + total_written, len - total_written);
|
||||||
|
if (n < 0) {
|
||||||
|
int err = errno;
|
||||||
|
if (err == EAGAIN || err == EWOULDBLOCK) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (err == EPIPE || err == ECONNRESET) {
|
||||||
|
close(g_child_state.socket_fd);
|
||||||
|
g_child_state.socket_fd = -1;
|
||||||
|
g_child_state.connect_failed = 1;
|
||||||
|
|
||||||
|
apr_time_t now = apr_time_now();
|
||||||
|
if ((now - g_child_state.last_error_report) >= apr_time_from_sec(cfg->error_report_interval)) {
|
||||||
|
ap_log_error(APLOG_MARK, APLOG_ERR, err, s,
|
||||||
|
MOD_REQIN_LOG_NAME ": Unix socket write failed: %s", strerror(err));
|
||||||
|
g_child_state.last_error_report = now;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
total_written += n;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============== Request Logging Functions ============== */
|
||||||
|
|
||||||
|
static const char *get_header(request_rec *r, const char *name)
|
||||||
|
{
|
||||||
|
const apr_table_t *headers = r->headers_in;
|
||||||
|
apr_table_entry_t *elts = (apr_table_entry_t *)apr_table_elts(headers)->elts;
|
||||||
|
int nelts = apr_table_elts(headers)->nelts;
|
||||||
|
|
||||||
|
for (int i = 0; i < nelts; i++) {
|
||||||
|
if (strcasecmp(elts[i].key, name) == 0) {
|
||||||
|
return elts[i].val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void log_request(request_rec *r, reqin_log_config_t *cfg)
|
||||||
|
{
|
||||||
|
apr_pool_t *pool = r->pool;
|
||||||
|
server_rec *s = r->server;
|
||||||
|
char port_buf[16];
|
||||||
|
|
||||||
|
if (ensure_connected(cfg, s) < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dynbuf_t buf;
|
||||||
|
dynbuf_init(&buf, pool, 4096);
|
||||||
|
|
||||||
|
dynbuf_append(&buf, "{", 1);
|
||||||
|
|
||||||
|
/* time */
|
||||||
|
dynbuf_append(&buf, "\"time\":\"", 8);
|
||||||
|
format_iso8601(&buf, r->request_time);
|
||||||
|
dynbuf_append(&buf, "\",", 2);
|
||||||
|
|
||||||
|
/* timestamp */
|
||||||
|
apr_time_t now = apr_time_now();
|
||||||
|
apr_uint64_t ns = (apr_uint64_t)now * 1000;
|
||||||
|
char ts_buf[32];
|
||||||
|
snprintf(ts_buf, sizeof(ts_buf), "%" APR_UINT64_T_FMT, ns);
|
||||||
|
dynbuf_append(&buf, "\"timestamp\":", 12);
|
||||||
|
dynbuf_append(&buf, ts_buf, -1);
|
||||||
|
dynbuf_append(&buf, ",", 1);
|
||||||
|
|
||||||
|
/* src_ip */
|
||||||
|
dynbuf_append(&buf, "\"src_ip\":\"", 10);
|
||||||
|
dynbuf_append(&buf, r->useragent_ip ? r->useragent_ip : r->connection->client_ip, -1);
|
||||||
|
dynbuf_append(&buf, "\",", 2);
|
||||||
|
|
||||||
|
/* src_port */
|
||||||
|
port_buf[0] = '\0';
|
||||||
|
if (r->connection->client_addr != NULL) {
|
||||||
|
snprintf(port_buf, sizeof(port_buf), "%u", r->connection->client_addr->port);
|
||||||
|
}
|
||||||
|
dynbuf_append(&buf, "\"src_port\":", 11);
|
||||||
|
dynbuf_append(&buf, port_buf, -1);
|
||||||
|
dynbuf_append(&buf, ",", 1);
|
||||||
|
|
||||||
|
/* dst_ip */
|
||||||
|
dynbuf_append(&buf, "\"dst_ip\":\"", 10);
|
||||||
|
dynbuf_append(&buf, r->connection->local_ip, -1);
|
||||||
|
dynbuf_append(&buf, "\",", 2);
|
||||||
|
|
||||||
|
/* dst_port */
|
||||||
|
port_buf[0] = '\0';
|
||||||
|
if (r->connection->local_addr != NULL) {
|
||||||
|
snprintf(port_buf, sizeof(port_buf), "%u", r->connection->local_addr->port);
|
||||||
|
}
|
||||||
|
dynbuf_append(&buf, "\"dst_port\":", 11);
|
||||||
|
dynbuf_append(&buf, port_buf, -1);
|
||||||
|
dynbuf_append(&buf, ",", 1);
|
||||||
|
|
||||||
|
/* method */
|
||||||
|
dynbuf_append(&buf, "\"method\":\"", 10);
|
||||||
|
append_json_string(&buf, r->method);
|
||||||
|
dynbuf_append(&buf, "\",", 2);
|
||||||
|
|
||||||
|
/* path */
|
||||||
|
dynbuf_append(&buf, "\"path\":\"", 8);
|
||||||
|
append_json_string(&buf, r->parsed_uri.path ? r->parsed_uri.path : "/");
|
||||||
|
dynbuf_append(&buf, "\",", 2);
|
||||||
|
|
||||||
|
/* host */
|
||||||
|
const char *host = apr_table_get(r->headers_in, "Host");
|
||||||
|
dynbuf_append(&buf, "\"host\":\"", 8);
|
||||||
|
append_json_string(&buf, host ? host : "");
|
||||||
|
dynbuf_append(&buf, "\",", 2);
|
||||||
|
|
||||||
|
/* http_version */
|
||||||
|
dynbuf_append(&buf, "\"http_version\":\"", 16);
|
||||||
|
dynbuf_append(&buf, r->protocol, -1);
|
||||||
|
dynbuf_append(&buf, "\"", 1);
|
||||||
|
|
||||||
|
/* headers - flat structure at same level as other fields */
|
||||||
|
if (cfg->headers && cfg->headers->nelts > 0) {
|
||||||
|
int header_count = 0;
|
||||||
|
int max_to_log = cfg->max_headers;
|
||||||
|
const char **header_names = (const char **)cfg->headers->elts;
|
||||||
|
|
||||||
|
for (int i = 0; i < cfg->headers->nelts && header_count < max_to_log; i++) {
|
||||||
|
const char *header_name = header_names[i];
|
||||||
|
const char *header_value = get_header(r, header_name);
|
||||||
|
|
||||||
|
if (header_value != NULL) {
|
||||||
|
dynbuf_append(&buf, ",\"header_", 9);
|
||||||
|
append_json_string(&buf, header_name);
|
||||||
|
dynbuf_append(&buf, "\":\"", 3);
|
||||||
|
|
||||||
|
apr_size_t val_len = strlen(header_value);
|
||||||
|
if ((int)val_len > cfg->max_header_value_len) {
|
||||||
|
val_len = cfg->max_header_value_len;
|
||||||
|
}
|
||||||
|
|
||||||
|
char *truncated = apr_pstrmemdup(pool, header_value, val_len);
|
||||||
|
append_json_string(&buf, truncated);
|
||||||
|
dynbuf_append(&buf, "\"", 1);
|
||||||
|
|
||||||
|
header_count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dynbuf_append(&buf, "}\n", 2);
|
||||||
|
|
||||||
|
write_to_socket(buf.data, buf.len, s, cfg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============== Apache Hooks ============== */
|
||||||
|
|
||||||
|
static int reqin_log_post_read_request(request_rec *r)
|
||||||
|
{
|
||||||
|
reqin_log_config_t *cfg = get_module_config(r->server);
|
||||||
|
|
||||||
|
if (cfg == NULL || !cfg->enabled || cfg->socket_path == NULL) {
|
||||||
|
return DECLINED;
|
||||||
|
}
|
||||||
|
|
||||||
|
log_request(r, cfg);
|
||||||
|
return DECLINED;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void reqin_log_child_init(apr_pool_t *p, server_rec *s)
|
||||||
|
{
|
||||||
|
(void)p;
|
||||||
|
|
||||||
|
reqin_log_config_t *cfg = get_module_config(s);
|
||||||
|
|
||||||
|
g_child_state.socket_fd = -1;
|
||||||
|
g_child_state.last_connect_attempt = 0;
|
||||||
|
g_child_state.last_error_report = 0;
|
||||||
|
g_child_state.connect_failed = 0;
|
||||||
|
|
||||||
|
if (cfg == NULL || !cfg->enabled || cfg->socket_path == NULL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try_connect(cfg, s);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void reqin_log_register_hooks(apr_pool_t *p)
|
||||||
|
{
|
||||||
|
(void)p;
|
||||||
|
ap_hook_post_read_request(reqin_log_post_read_request, NULL, NULL, APR_HOOK_MIDDLE);
|
||||||
|
ap_hook_child_init(reqin_log_child_init, NULL, NULL, APR_HOOK_MIDDLE);
|
||||||
|
}
|
||||||
36
src/mod_reqin_log.h
Normal file
36
src/mod_reqin_log.h
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
/*
|
||||||
|
* 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"
|
||||||
|
|
||||||
|
/* Module name */
|
||||||
|
#define MOD_REQIN_LOG_NAME "mod_reqin_log"
|
||||||
|
|
||||||
|
/* 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
|
||||||
|
|
||||||
|
/* 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 */
|
||||||
215
tests/unit/test_config_parsing.c
Normal file
215
tests/unit/test_config_parsing.c
Normal file
@ -0,0 +1,215 @@
|
|||||||
|
/*
|
||||||
|
* 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>
|
||||||
|
|
||||||
|
/* 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
|
||||||
|
|
||||||
|
/* 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;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int parse_max_headers(const char *value, int *result)
|
||||||
|
{
|
||||||
|
char *endptr;
|
||||||
|
long val = strtol(value, &endptr, 10);
|
||||||
|
if (*endptr != '\0' || val < 0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
*result = (int)val;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int parse_interval(const char *value, int *result)
|
||||||
|
{
|
||||||
|
char *endptr;
|
||||||
|
long val = strtol(value, &endptr, 10);
|
||||||
|
if (*endptr != '\0' || val < 0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
*result = (int)val;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Test: Parse enabled On */
|
||||||
|
static void test_parse_enabled_on(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)
|
||||||
|
{
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
const char *result = parse_socket_path("/var/run/mod_reqin_log.sock");
|
||||||
|
assert_string_equal(result, "/var/run/mod_reqin_log.sock");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Test: Parse socket path empty */
|
||||||
|
static void test_parse_socket_path_empty(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)
|
||||||
|
{
|
||||||
|
const char *result = parse_socket_path(NULL);
|
||||||
|
assert_null(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Test: Parse max headers valid */
|
||||||
|
static void test_parse_max_headers_valid(void **state)
|
||||||
|
{
|
||||||
|
int result;
|
||||||
|
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;
|
||||||
|
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;
|
||||||
|
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;
|
||||||
|
assert_int_equal(parse_interval("-5", &result), -1);
|
||||||
|
assert_int_equal(parse_interval("abc", &result), -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Test: Default configuration values */
|
||||||
|
static void test_default_config_values(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)
|
||||||
|
{
|
||||||
|
/* Valid: enabled with socket */
|
||||||
|
int enabled = 1;
|
||||||
|
const char *socket = "/var/run/socket";
|
||||||
|
assert_true(enabled == 0 || socket != NULL);
|
||||||
|
|
||||||
|
/* Invalid: enabled without socket */
|
||||||
|
socket = NULL;
|
||||||
|
assert_false(enabled == 0 || socket != NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Test: Header value length validation */
|
||||||
|
static void test_header_value_len_validation(void **state)
|
||||||
|
{
|
||||||
|
int result;
|
||||||
|
assert_int_equal(parse_interval("1", &result), 0);
|
||||||
|
assert_true(result >= 1);
|
||||||
|
|
||||||
|
assert_int_equal(parse_interval("0", &result), 0);
|
||||||
|
assert_false(result >= 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Test: Large but valid values */
|
||||||
|
static void test_large_valid_values(void **state)
|
||||||
|
{
|
||||||
|
int result;
|
||||||
|
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_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_default_config_values),
|
||||||
|
cmocka_unit_test(test_config_validation_enabled_requires_socket),
|
||||||
|
cmocka_unit_test(test_header_value_len_validation),
|
||||||
|
cmocka_unit_test(test_large_valid_values),
|
||||||
|
};
|
||||||
|
|
||||||
|
return cmocka_run_group_tests(tests, NULL, NULL);
|
||||||
|
}
|
||||||
211
tests/unit/test_header_handling.c
Normal file
211
tests/unit/test_header_handling.c
Normal file
@ -0,0 +1,211 @@
|
|||||||
|
/*
|
||||||
|
* 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>
|
||||||
|
|
||||||
|
/* 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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, NULL, NULL);
|
||||||
|
}
|
||||||
198
tests/unit/test_json_serialization.c
Normal file
198
tests/unit/test_json_serialization.c
Normal file
@ -0,0 +1,198 @@
|
|||||||
|
/*
|
||||||
|
* 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_strings.h>
|
||||||
|
#include <apr_time.h>
|
||||||
|
#include <apr_lib.h>
|
||||||
|
|
||||||
|
/* Mock JSON string escaping function for testing */
|
||||||
|
static void append_json_string(apr_pool_t *pool, apr_strbuf_t *buf, const char *str)
|
||||||
|
{
|
||||||
|
if (str == NULL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const char *p = str; *p; p++) {
|
||||||
|
char c = *p;
|
||||||
|
switch (c) {
|
||||||
|
case '"': apr_strbuf_append(buf, "\\\"", 2); break;
|
||||||
|
case '\\': apr_strbuf_append(buf, "\\\\", 2); break;
|
||||||
|
case '\b': apr_strbuf_append(buf, "\\b", 2); break;
|
||||||
|
case '\f': apr_strbuf_append(buf, "\\f", 2); break;
|
||||||
|
case '\n': apr_strbuf_append(buf, "\\n", 2); break;
|
||||||
|
case '\r': apr_strbuf_append(buf, "\\r", 2); break;
|
||||||
|
case '\t': apr_strbuf_append(buf, "\\t", 2); break;
|
||||||
|
default:
|
||||||
|
if ((unsigned char)c < 0x20) {
|
||||||
|
char unicode[8];
|
||||||
|
apr_snprintf(unicode, sizeof(unicode), "\\u%04x", (unsigned char)c);
|
||||||
|
apr_strbuf_append(buf, unicode, -1);
|
||||||
|
} else {
|
||||||
|
apr_strbuf_append_char(buf, c);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Test: Empty string */
|
||||||
|
static void test_json_escape_empty_string(void **state)
|
||||||
|
{
|
||||||
|
apr_pool_t *pool;
|
||||||
|
apr_pool_create(&pool, NULL);
|
||||||
|
|
||||||
|
apr_strbuf_t buf;
|
||||||
|
char *initial = apr_palloc(pool, 256);
|
||||||
|
apr_strbuf_init(pool, &buf, initial, 256);
|
||||||
|
|
||||||
|
append_json_string(pool, &buf, "");
|
||||||
|
|
||||||
|
assert_string_equal(buf.buf, "");
|
||||||
|
|
||||||
|
apr_pool_destroy(pool);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Test: Simple string without special characters */
|
||||||
|
static void test_json_escape_simple_string(void **state)
|
||||||
|
{
|
||||||
|
apr_pool_t *pool;
|
||||||
|
apr_pool_create(&pool, NULL);
|
||||||
|
|
||||||
|
apr_strbuf_t buf;
|
||||||
|
char *initial = apr_palloc(pool, 256);
|
||||||
|
apr_strbuf_init(pool, &buf, initial, 256);
|
||||||
|
|
||||||
|
append_json_string(pool, &buf, "hello world");
|
||||||
|
|
||||||
|
assert_string_equal(buf.buf, "hello world");
|
||||||
|
|
||||||
|
apr_pool_destroy(pool);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Test: String with double quotes */
|
||||||
|
static void test_json_escape_quotes(void **state)
|
||||||
|
{
|
||||||
|
apr_pool_t *pool;
|
||||||
|
apr_pool_create(&pool, NULL);
|
||||||
|
|
||||||
|
apr_strbuf_t buf;
|
||||||
|
char *initial = apr_palloc(pool, 256);
|
||||||
|
apr_strbuf_init(pool, &buf, initial, 256);
|
||||||
|
|
||||||
|
append_json_string(pool, &buf, "hello \"world\"");
|
||||||
|
|
||||||
|
assert_string_equal(buf.buf, "hello \\\"world\\\"");
|
||||||
|
|
||||||
|
apr_pool_destroy(pool);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Test: String with backslashes */
|
||||||
|
static void test_json_escape_backslashes(void **state)
|
||||||
|
{
|
||||||
|
apr_pool_t *pool;
|
||||||
|
apr_pool_create(&pool, NULL);
|
||||||
|
|
||||||
|
apr_strbuf_t buf;
|
||||||
|
char *initial = apr_palloc(pool, 256);
|
||||||
|
apr_strbuf_init(pool, &buf, initial, 256);
|
||||||
|
|
||||||
|
append_json_string(pool, &buf, "path\\to\\file");
|
||||||
|
|
||||||
|
assert_string_equal(buf.buf, "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;
|
||||||
|
apr_pool_create(&pool, NULL);
|
||||||
|
|
||||||
|
apr_strbuf_t buf;
|
||||||
|
char *initial = apr_palloc(pool, 256);
|
||||||
|
apr_strbuf_init(pool, &buf, initial, 256);
|
||||||
|
|
||||||
|
append_json_string(pool, &buf, "line1\nline2\ttab");
|
||||||
|
|
||||||
|
assert_string_equal(buf.buf, "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;
|
||||||
|
apr_pool_create(&pool, NULL);
|
||||||
|
|
||||||
|
apr_strbuf_t buf;
|
||||||
|
char *initial = apr_palloc(pool, 256);
|
||||||
|
apr_strbuf_init(pool, &buf, initial, 256);
|
||||||
|
|
||||||
|
/* Test with bell character (0x07) */
|
||||||
|
append_json_string(pool, &buf, "test\bell");
|
||||||
|
|
||||||
|
/* Should contain unicode escape */
|
||||||
|
assert_true(strstr(buf.buf, "\\u0007") != NULL);
|
||||||
|
|
||||||
|
apr_pool_destroy(pool);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Test: NULL string */
|
||||||
|
static void test_json_escape_null_string(void **state)
|
||||||
|
{
|
||||||
|
apr_pool_t *pool;
|
||||||
|
apr_pool_create(&pool, NULL);
|
||||||
|
|
||||||
|
apr_strbuf_t buf;
|
||||||
|
char *initial = apr_palloc(pool, 256);
|
||||||
|
apr_strbuf_init(pool, &buf, initial, 256);
|
||||||
|
|
||||||
|
append_json_string(pool, &buf, NULL);
|
||||||
|
|
||||||
|
assert_string_equal(buf.buf, "");
|
||||||
|
|
||||||
|
apr_pool_destroy(pool);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Test: Complex user agent string */
|
||||||
|
static void test_json_escape_user_agent(void **state)
|
||||||
|
{
|
||||||
|
apr_pool_t *pool;
|
||||||
|
apr_pool_create(&pool, NULL);
|
||||||
|
|
||||||
|
apr_strbuf_t buf;
|
||||||
|
char *initial = apr_palloc(pool, 512);
|
||||||
|
apr_strbuf_init(pool, &buf, initial, 512);
|
||||||
|
|
||||||
|
const char *ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) \"Test\"";
|
||||||
|
append_json_string(pool, &buf, ua);
|
||||||
|
|
||||||
|
assert_true(strstr(buf.buf, "\\\"Test\\\"") != NULL);
|
||||||
|
|
||||||
|
apr_pool_destroy(pool);
|
||||||
|
}
|
||||||
|
|
||||||
|
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, NULL, NULL);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user