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

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