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