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:
Jacquin Antoine
2026-02-26 13:55:07 +01:00
commit 66549acf5c
27 changed files with 3550 additions and 0 deletions

202
.github/workflows/ci.yml vendored Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View File

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

86
Makefile Normal file
View 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
View 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
View 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
View 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
View File

@ -0,0 +1 @@
10

27
packaging/deb/control Normal file
View 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
View 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
View 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/

View 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
View 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
View 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
View 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())

View 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
View 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
View 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
View 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
View 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 */

View 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);
}

View 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);
}

View 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);
}