- File header: French multi-line description block - 7 section banners in French (/* ====== Section ====== */ format): Configuration du serveur, Buffer dynamique, Sérialisation JSON, Gestionnaires de directives, Socket Unix, Journalisation, Hooks Apache - 26 @brief/@param/@return blocks on every function: server config, dynbuf_*, JSON helpers, cmd_set_* handlers, socket helpers (try_connect/ensure_connected/write_to_socket), log_request, Apache hooks (post_read_request, child_init, etc.) - No logic changes (1033 → 1268 lines, comments only) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1269 lines
43 KiB
C
1269 lines
43 KiB
C
/* mod_reqin_log.c — Module Apache HTTPD pour la journalisation des requêtes HTTP
|
|
* entrantes au format JSON vers un socket de domaine Unix.
|
|
*
|
|
* Fonctionnalités : capture des requêtes en phase post-read, sérialisation JSON,
|
|
* envoi non-bloquant vers un socket Unix, reconnexion automatique et filtrage
|
|
* des en-têtes sensibles.
|
|
*
|
|
* 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 "mod_reqin_log.h"
|
|
#include "apr_strings.h"
|
|
#include "apr_time.h"
|
|
#include "apr_lib.h"
|
|
#include "ap_config.h"
|
|
#include "ap_mpm.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>
|
|
#include <stdlib.h>
|
|
#include <limits.h>
|
|
|
|
#ifndef MSG_NOSIGNAL
|
|
#define MSG_NOSIGNAL 0
|
|
#endif
|
|
|
|
/* Maximum Unix socket path length (sun_path is typically 108 bytes) */
|
|
#define MAX_SOCKET_PATH_LEN (sizeof(((struct sockaddr_un *)0)->sun_path) - 1)
|
|
|
|
/* Maximum JSON log line size (64KB) - prevents memory exhaustion DoS */
|
|
#define MAX_JSON_SIZE (64 * 1024)
|
|
|
|
/* Helper macro for throttled error logging - prevents error_log flooding */
|
|
#define LOG_THROTTLED(state, cfg, s, level, err, msg, ...) do { \
|
|
apr_time_t lt_now = apr_time_now(); \
|
|
int lt_should_report = 0; \
|
|
FD_MUTEX_LOCK(state); \
|
|
if ((lt_now - state->last_error_report) >= apr_time_from_sec(cfg->error_report_interval)) { \
|
|
state->last_error_report = lt_now; \
|
|
lt_should_report = 1; \
|
|
} \
|
|
FD_MUTEX_UNLOCK(state); \
|
|
if (lt_should_report) { \
|
|
ap_log_error(APLOG_MARK, level, err, s, MOD_REQIN_LOG_NAME ": " msg, ##__VA_ARGS__); \
|
|
} \
|
|
} while(0)
|
|
|
|
/* Helper macro to check if a log level should be emitted */
|
|
#define SHOULD_LOG(srv_conf, level) ((srv_conf) && (srv_conf)->log_level <= (level))
|
|
|
|
/* Default sensitive headers blacklist - prevents accidental logging of credentials */
|
|
static const char *const DEFAULT_SENSITIVE_HEADERS[] = {
|
|
"Authorization",
|
|
"Cookie",
|
|
"Set-Cookie",
|
|
"X-Api-Key",
|
|
"X-Auth-Token",
|
|
"Proxy-Authorization",
|
|
"WWW-Authenticate",
|
|
NULL
|
|
};
|
|
|
|
/* 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 - stored in server config
|
|
* Includes mutex for thread safety in worker/event MPMs */
|
|
typedef struct {
|
|
int socket_fd;
|
|
apr_thread_mutex_t *fd_mutex; /* Protects socket_fd from concurrent access */
|
|
apr_time_t last_connect_attempt;
|
|
apr_time_t last_error_report;
|
|
int connect_failed;
|
|
} reqin_log_child_state_t;
|
|
|
|
/* Log levels */
|
|
typedef enum {
|
|
REQIN_LOG_LEVEL_DEBUG = 0,
|
|
REQIN_LOG_LEVEL_INFO = 1,
|
|
REQIN_LOG_LEVEL_WARNING = 2,
|
|
REQIN_LOG_LEVEL_ERROR = 3,
|
|
REQIN_LOG_LEVEL_EMERG = 4
|
|
} reqin_log_level_t;
|
|
|
|
/* Module server configuration structure */
|
|
typedef struct {
|
|
reqin_log_config_t *config;
|
|
reqin_log_child_state_t child_state;
|
|
reqin_log_level_t log_level;
|
|
} reqin_log_server_conf_t;
|
|
|
|
/* 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);
|
|
static int parse_int_strict(const char *arg, int *out);
|
|
|
|
/* Forward declarations for server config */
|
|
static void *reqin_log_create_server_conf(apr_pool_t *pool, server_rec *s);
|
|
|
|
/* Forward declarations for commands */
|
|
static const char *cmd_set_enabled(cmd_parms *cmd, void *dummy, int flag);
|
|
static const char *cmd_set_socket(cmd_parms *cmd, void *dummy, const char *arg);
|
|
static const char *cmd_set_headers(cmd_parms *cmd, void *dummy, const char *arg);
|
|
static const char *cmd_set_max_headers(cmd_parms *cmd, void *dummy, const char *arg);
|
|
static const char *cmd_set_max_header_value_len(cmd_parms *cmd, void *dummy, const char *arg);
|
|
static const char *cmd_set_reconnect_interval(cmd_parms *cmd, void *dummy, const char *arg);
|
|
static const char *cmd_set_error_report_interval(cmd_parms *cmd, void *dummy, const char *arg);
|
|
static const char *cmd_set_log_level(cmd_parms *cmd, void *dummy, 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 int reqin_log_post_config(apr_pool_t *pconf, apr_pool_t *plog, apr_pool_t *ptemp, 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)"),
|
|
AP_INIT_TAKE1("JsonSockLogLevel", cmd_set_log_level, NULL, RSRC_CONF,
|
|
"Log level: DEBUG, INFO, WARNING, ERROR, EMERG (default: WARNING)"),
|
|
{ NULL }
|
|
};
|
|
|
|
/* Module definition */
|
|
module AP_MODULE_DECLARE_DATA reqin_log_module = {
|
|
STANDARD20_MODULE_STUFF,
|
|
NULL, /* per-directory config creator */
|
|
NULL, /* dir config merger */
|
|
reqin_log_create_server_conf, /* server config creator */
|
|
NULL, /* server config merger */
|
|
reqin_log_cmds, /* command table */
|
|
reqin_log_register_hooks, /* register hooks */
|
|
0 /* flags */
|
|
};
|
|
|
|
/* ====== Configuration du serveur ====== */
|
|
|
|
/**
|
|
* @brief Retourne la configuration du module pour un serveur virtuel donné.
|
|
*
|
|
* @param s Enregistrement du serveur Apache.
|
|
* @return Pointeur vers la configuration du serveur, ou NULL.
|
|
*/
|
|
static reqin_log_server_conf_t *get_server_conf(server_rec *s)
|
|
{
|
|
return (reqin_log_server_conf_t *)ap_get_module_config(s->module_config, &reqin_log_module);
|
|
}
|
|
|
|
/**
|
|
* @brief Crée et initialise la configuration du serveur avec les valeurs par défaut.
|
|
*
|
|
* @param pool Pool APR pour les allocations mémoire.
|
|
* @param s Enregistrement du serveur (non utilisé).
|
|
* @return Pointeur opaque vers la configuration initialisée.
|
|
*/
|
|
static void *reqin_log_create_server_conf(apr_pool_t *pool, server_rec *s)
|
|
{
|
|
(void)s;
|
|
reqin_log_server_conf_t *srv_conf = apr_pcalloc(pool, sizeof(reqin_log_server_conf_t));
|
|
|
|
srv_conf->config = apr_pcalloc(pool, sizeof(reqin_log_config_t));
|
|
srv_conf->config->headers = apr_array_make(pool, 5, sizeof(const char *));
|
|
srv_conf->config->max_headers = DEFAULT_MAX_HEADERS;
|
|
srv_conf->config->max_header_value_len = DEFAULT_MAX_HEADER_VALUE_LEN;
|
|
srv_conf->config->reconnect_interval = DEFAULT_RECONNECT_INTERVAL;
|
|
srv_conf->config->error_report_interval = DEFAULT_ERROR_REPORT_INTERVAL;
|
|
|
|
srv_conf->child_state.socket_fd = -1;
|
|
srv_conf->child_state.fd_mutex = NULL;
|
|
srv_conf->child_state.last_connect_attempt = 0;
|
|
srv_conf->child_state.last_error_report = 0;
|
|
srv_conf->child_state.connect_failed = 0;
|
|
|
|
srv_conf->log_level = REQIN_LOG_LEVEL_WARNING;
|
|
|
|
return srv_conf;
|
|
}
|
|
|
|
/* ====== Fonctions de buffer dynamique ====== */
|
|
|
|
/**
|
|
* @brief Initialise le buffer dynamique avec la capacité initiale spécifiée.
|
|
*
|
|
* @param db Pointeur vers le buffer dynamique à initialiser.
|
|
* @param pool Pool APR utilisé pour toutes les allocations mémoire.
|
|
* @param initial_capacity Capacité initiale en octets.
|
|
*/
|
|
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';
|
|
}
|
|
|
|
/**
|
|
* @brief Ajoute une chaîne au buffer dynamique, en le redimensionnant si nécessaire.
|
|
*
|
|
* @param db Pointeur vers le buffer dynamique.
|
|
* @param str Chaîne à ajouter (ignorée si NULL).
|
|
* @param len Nombre d'octets à copier, ou (apr_size_t)-1 pour utiliser strlen(str).
|
|
*/
|
|
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 + 1); /* Copy including null terminator */
|
|
db->data = new_data;
|
|
db->capacity = new_capacity;
|
|
}
|
|
|
|
memcpy(db->data + db->len, str, len);
|
|
db->len += len;
|
|
db->data[db->len] = '\0';
|
|
}
|
|
|
|
/**
|
|
* @brief Ajoute un seul caractère au buffer dynamique, en le redimensionnant si nécessaire.
|
|
*
|
|
* @param db Pointeur vers le buffer dynamique.
|
|
* @param c Caractère à ajouter.
|
|
*/
|
|
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';
|
|
}
|
|
|
|
/* ====== Sérialisation JSON ====== */
|
|
|
|
/**
|
|
* @brief Échappe et ajoute une chaîne C dans le buffer au format JSON (RFC 7159).
|
|
*
|
|
* Les caractères spéciaux (guillemets, antislash, retours chariot, etc.) sont encodés.
|
|
* Les caractères de contrôle inférieurs à 0x20 sont produits sous la forme \\uXXXX.
|
|
*
|
|
* @param db Pointeur vers le buffer dynamique de destination.
|
|
* @param str Chaîne source à sérialiser (ignorée si NULL).
|
|
*/
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Formate un horodatage APR en chaîne ISO 8601 UTC et l'ajoute au buffer.
|
|
*
|
|
* Le format produit est YYYY-MM-DDTHH:MM:SSZ (20 caractères).
|
|
* Les composantes hors plage sont écrêtées pour éviter tout dépassement de tampon.
|
|
*
|
|
* @param db Pointeur vers le buffer dynamique de destination.
|
|
* @param t Horodatage APR en microsecondes depuis l'époque Unix.
|
|
*/
|
|
static void format_iso8601(dynbuf_t *db, apr_time_t t)
|
|
{
|
|
apr_time_exp_t tm;
|
|
apr_time_exp_gmt(&tm, t);
|
|
|
|
/* Validate time components to prevent buffer overflow and invalid output.
|
|
* apr_time_exp_gmt should always produce valid values, but we validate
|
|
* defensively to catch any APR bugs or memory corruption. */
|
|
int year = tm.tm_year + 1900;
|
|
int mon = tm.tm_mon + 1;
|
|
int day = tm.tm_mday;
|
|
int hour = tm.tm_hour;
|
|
int min = tm.tm_min;
|
|
int sec = tm.tm_sec;
|
|
|
|
/* Clamp values to valid ranges to ensure fixed-width output (20 chars + null) */
|
|
if (year < 0) year = 0;
|
|
if (year > 9999) year = 9999;
|
|
if (mon < 1) mon = 1;
|
|
if (mon > 12) mon = 12;
|
|
if (day < 1) day = 1;
|
|
if (day > 31) day = 31;
|
|
if (hour < 0) hour = 0;
|
|
if (hour > 23) hour = 23;
|
|
if (min < 0) min = 0;
|
|
if (min > 59) min = 59;
|
|
if (sec < 0) sec = 0;
|
|
if (sec > 61) sec = 61;
|
|
|
|
char time_str[32];
|
|
snprintf(time_str, sizeof(time_str), "%04d-%02d-%02dT%02d:%02d:%02dZ",
|
|
year, mon, day, hour, min, sec);
|
|
dynbuf_append(db, time_str, -1);
|
|
}
|
|
|
|
/**
|
|
* @brief Analyse strictement un entier décimal depuis une chaîne de caractères.
|
|
*
|
|
* Rejette toute chaîne contenant des espaces en tête, des caractères non numériques
|
|
* ou des valeurs hors de la plage [INT_MIN, INT_MAX].
|
|
*
|
|
* @param arg Chaîne à analyser.
|
|
* @param out Pointeur vers l'entier résultat (renseigné en cas de succès).
|
|
* @return 0 en cas de succès, -1 en cas d'erreur.
|
|
*/
|
|
static int parse_int_strict(const char *arg, int *out)
|
|
{
|
|
char *end = NULL;
|
|
long v;
|
|
if (arg == NULL || *arg == '\0' || out == NULL) return -1;
|
|
|
|
/* Reject leading whitespace (strtol skips it by default) */
|
|
if (apr_isspace(*arg)) return -1;
|
|
|
|
errno = 0;
|
|
v = strtol(arg, &end, 10);
|
|
if (errno != 0 || end == arg || *end != '\0' || v < INT_MIN || v > INT_MAX) return -1;
|
|
*out = (int)v;
|
|
return 0;
|
|
}
|
|
|
|
/* ====== Gestionnaires de directives de configuration ====== */
|
|
|
|
/**
|
|
* @brief Gestionnaire de la directive JsonSockLogEnabled — active ou désactive le module.
|
|
*
|
|
* @param cmd Contexte de la directive Apache.
|
|
* @param dummy Contexte de répertoire (non utilisé).
|
|
* @param flag 1 pour activer le module, 0 pour le désactiver.
|
|
* @return NULL en cas de succès, message d'erreur statique sinon.
|
|
*/
|
|
static const char *cmd_set_enabled(cmd_parms *cmd, void *dummy, int flag)
|
|
{
|
|
(void)dummy;
|
|
reqin_log_server_conf_t *srv_conf = get_server_conf(cmd->server);
|
|
if (srv_conf == NULL) {
|
|
return "Internal error: server configuration not available";
|
|
}
|
|
srv_conf->config->enabled = flag ? 1 : 0;
|
|
return NULL;
|
|
}
|
|
|
|
/**
|
|
* @brief Gestionnaire de la directive JsonSockLogSocket — définit le chemin du socket Unix.
|
|
*
|
|
* @param cmd Contexte de la directive Apache.
|
|
* @param dummy Contexte de répertoire (non utilisé).
|
|
* @param arg Chemin absolu vers le socket de domaine Unix.
|
|
* @return NULL en cas de succès, message d'erreur statique sinon.
|
|
*/
|
|
static const char *cmd_set_socket(cmd_parms *cmd, void *dummy, const char *arg)
|
|
{
|
|
(void)dummy;
|
|
reqin_log_server_conf_t *srv_conf = get_server_conf(cmd->server);
|
|
if (srv_conf == NULL) {
|
|
return "Internal error: server configuration not available";
|
|
}
|
|
if (arg == NULL || *arg == '\0') {
|
|
return "JsonSockLogSocket must be a non-empty Unix socket path";
|
|
}
|
|
if (strlen(arg) >= MAX_SOCKET_PATH_LEN) {
|
|
return "JsonSockLogSocket path is too long for Unix domain socket";
|
|
}
|
|
srv_conf->config->socket_path = apr_pstrdup(cmd->pool, arg);
|
|
return NULL;
|
|
}
|
|
|
|
/**
|
|
* @brief Gestionnaire de la directive JsonSockLogHeaders — ajoute un en-tête à journaliser.
|
|
*
|
|
* Peut être appelé plusieurs fois (directive ITERATE) pour constituer la liste
|
|
* des en-têtes HTTP à inclure dans le JSON produit.
|
|
*
|
|
* @param cmd Contexte de la directive Apache.
|
|
* @param dummy Contexte de répertoire (non utilisé).
|
|
* @param arg Nom de l'en-tête HTTP à ajouter à la liste.
|
|
* @return NULL en cas de succès, message d'erreur statique sinon.
|
|
*/
|
|
static const char *cmd_set_headers(cmd_parms *cmd, void *dummy, const char *arg)
|
|
{
|
|
(void)dummy;
|
|
reqin_log_server_conf_t *srv_conf = get_server_conf(cmd->server);
|
|
if (srv_conf == NULL) {
|
|
return "Internal error: server configuration not available";
|
|
}
|
|
*(const char **)apr_array_push(srv_conf->config->headers) = apr_pstrdup(cmd->pool, arg);
|
|
return NULL;
|
|
}
|
|
|
|
/**
|
|
* @brief Gestionnaire de la directive JsonSockLogMaxHeaders — limite le nombre d'en-têtes journalisés.
|
|
*
|
|
* @param cmd Contexte de la directive Apache.
|
|
* @param dummy Contexte de répertoire (non utilisé).
|
|
* @param arg Nombre entier maximal d'en-têtes à journaliser (>= 0).
|
|
* @return NULL en cas de succès, message d'erreur statique sinon.
|
|
*/
|
|
static const char *cmd_set_max_headers(cmd_parms *cmd, void *dummy, const char *arg)
|
|
{
|
|
int val;
|
|
(void)dummy;
|
|
reqin_log_server_conf_t *srv_conf = get_server_conf(cmd->server);
|
|
if (srv_conf == NULL) {
|
|
return "Internal error: server configuration not available";
|
|
}
|
|
if (parse_int_strict(arg, &val) != 0) {
|
|
return "JsonSockLogMaxHeaders must be a valid integer";
|
|
}
|
|
if (val < 0) {
|
|
return "JsonSockLogMaxHeaders must be >= 0";
|
|
}
|
|
srv_conf->config->max_headers = val;
|
|
return NULL;
|
|
}
|
|
|
|
/**
|
|
* @brief Gestionnaire de la directive JsonSockLogMaxHeaderValueLen — limite la longueur des valeurs d'en-têtes.
|
|
*
|
|
* @param cmd Contexte de la directive Apache.
|
|
* @param dummy Contexte de répertoire (non utilisé).
|
|
* @param arg Longueur maximale en octets d'une valeur d'en-tête (>= 1).
|
|
* @return NULL en cas de succès, message d'erreur statique sinon.
|
|
*/
|
|
static const char *cmd_set_max_header_value_len(cmd_parms *cmd, void *dummy, const char *arg)
|
|
{
|
|
int val;
|
|
(void)dummy;
|
|
reqin_log_server_conf_t *srv_conf = get_server_conf(cmd->server);
|
|
if (srv_conf == NULL) {
|
|
return "Internal error: server configuration not available";
|
|
}
|
|
if (parse_int_strict(arg, &val) != 0) {
|
|
return "JsonSockLogMaxHeaderValueLen must be a valid integer";
|
|
}
|
|
if (val < 1) {
|
|
return "JsonSockLogMaxHeaderValueLen must be >= 1";
|
|
}
|
|
srv_conf->config->max_header_value_len = val;
|
|
return NULL;
|
|
}
|
|
|
|
/**
|
|
* @brief Gestionnaire de la directive JsonSockLogReconnectInterval — intervalle de reconnexion au socket.
|
|
*
|
|
* @param cmd Contexte de la directive Apache.
|
|
* @param dummy Contexte de répertoire (non utilisé).
|
|
* @param arg Intervalle en secondes entre deux tentatives de reconnexion (>= 0).
|
|
* @return NULL en cas de succès, message d'erreur statique sinon.
|
|
*/
|
|
static const char *cmd_set_reconnect_interval(cmd_parms *cmd, void *dummy, const char *arg)
|
|
{
|
|
int val;
|
|
(void)dummy;
|
|
reqin_log_server_conf_t *srv_conf = get_server_conf(cmd->server);
|
|
if (srv_conf == NULL) {
|
|
return "Internal error: server configuration not available";
|
|
}
|
|
if (parse_int_strict(arg, &val) != 0) {
|
|
return "JsonSockLogReconnectInterval must be a valid integer";
|
|
}
|
|
if (val < 0) {
|
|
return "JsonSockLogReconnectInterval must be >= 0";
|
|
}
|
|
srv_conf->config->reconnect_interval = val;
|
|
return NULL;
|
|
}
|
|
|
|
/**
|
|
* @brief Gestionnaire de la directive JsonSockLogErrorReportInterval — fréquence des erreurs dans error_log.
|
|
*
|
|
* @param cmd Contexte de la directive Apache.
|
|
* @param dummy Contexte de répertoire (non utilisé).
|
|
* @param arg Intervalle minimal en secondes entre deux entrées d'erreur (>= 0).
|
|
* @return NULL en cas de succès, message d'erreur statique sinon.
|
|
*/
|
|
static const char *cmd_set_error_report_interval(cmd_parms *cmd, void *dummy, const char *arg)
|
|
{
|
|
int val;
|
|
(void)dummy;
|
|
reqin_log_server_conf_t *srv_conf = get_server_conf(cmd->server);
|
|
if (srv_conf == NULL) {
|
|
return "Internal error: server configuration not available";
|
|
}
|
|
if (parse_int_strict(arg, &val) != 0) {
|
|
return "JsonSockLogErrorReportInterval must be a valid integer";
|
|
}
|
|
if (val < 0) {
|
|
return "JsonSockLogErrorReportInterval must be >= 0";
|
|
}
|
|
srv_conf->config->error_report_interval = val;
|
|
return NULL;
|
|
}
|
|
|
|
/**
|
|
* @brief Gestionnaire de la directive JsonSockLogLevel — définit le niveau de verbosité interne.
|
|
*
|
|
* Valeurs acceptées : DEBUG, INFO, WARNING, ERROR, EMERG (insensible à la casse).
|
|
*
|
|
* @param cmd Contexte de la directive Apache.
|
|
* @param dummy Contexte de répertoire (non utilisé).
|
|
* @param arg Chaîne représentant le niveau de log souhaité.
|
|
* @return NULL en cas de succès, message d'erreur statique sinon.
|
|
*/
|
|
static const char *cmd_set_log_level(cmd_parms *cmd, void *dummy, const char *arg)
|
|
{
|
|
(void)dummy;
|
|
reqin_log_server_conf_t *srv_conf = get_server_conf(cmd->server);
|
|
if (srv_conf == NULL) {
|
|
return "Internal error: server configuration not available";
|
|
}
|
|
if (arg == NULL || arg[0] == '\0') {
|
|
return "JsonSockLogLevel must be a non-empty string";
|
|
}
|
|
if (strcasecmp(arg, "DEBUG") == 0) {
|
|
srv_conf->log_level = REQIN_LOG_LEVEL_DEBUG;
|
|
} else if (strcasecmp(arg, "INFO") == 0) {
|
|
srv_conf->log_level = REQIN_LOG_LEVEL_INFO;
|
|
} else if (strcasecmp(arg, "WARNING") == 0) {
|
|
srv_conf->log_level = REQIN_LOG_LEVEL_WARNING;
|
|
} else if (strcasecmp(arg, "ERROR") == 0) {
|
|
srv_conf->log_level = REQIN_LOG_LEVEL_ERROR;
|
|
} else if (strcasecmp(arg, "EMERG") == 0) {
|
|
srv_conf->log_level = REQIN_LOG_LEVEL_EMERG;
|
|
} else {
|
|
return "JsonSockLogLevel must be one of: DEBUG, INFO, WARNING, ERROR, EMERG";
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
/* ====== Gestion du socket Unix ====== */
|
|
|
|
/**
|
|
* @brief Vérifie si un nom d'en-tête figure dans la liste noire des en-têtes sensibles.
|
|
*
|
|
* La comparaison est insensible à la casse. Les en-têtes sensibles (Authorization,
|
|
* Cookie, etc.) ne sont jamais journalisés pour éviter les fuites d'identifiants.
|
|
*
|
|
* @param name Nom de l'en-tête HTTP à vérifier.
|
|
* @return 1 si l'en-tête est sensible et doit être exclu, 0 sinon.
|
|
*/
|
|
static int is_sensitive_header(const char *name)
|
|
{
|
|
if (name == NULL) {
|
|
return 0;
|
|
}
|
|
|
|
for (int i = 0; DEFAULT_SENSITIVE_HEADERS[i] != NULL; i++) {
|
|
if (strcasecmp(name, DEFAULT_SENSITIVE_HEADERS[i]) == 0) {
|
|
return 1;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* Mutex helper macros for thread-safe socket FD access */
|
|
#define FD_MUTEX_LOCK(state) do { \
|
|
if ((state)->fd_mutex) { \
|
|
apr_thread_mutex_lock((state)->fd_mutex); \
|
|
} \
|
|
} while(0)
|
|
|
|
#define FD_MUTEX_UNLOCK(state) do { \
|
|
if ((state)->fd_mutex) { \
|
|
apr_thread_mutex_unlock((state)->fd_mutex); \
|
|
} \
|
|
} while(0)
|
|
|
|
/**
|
|
* @brief Tente d'établir (ou de rétablir) la connexion au socket Unix configuré.
|
|
*
|
|
* Respecte l'intervalle de reconnexion pour éviter de saturer les tentatives.
|
|
* Le socket est créé en mode non-bloquant (O_NONBLOCK). La fonction est protégée
|
|
* par le mutex interne pour la sécurité des threads.
|
|
*
|
|
* @param cfg Configuration du module (chemin du socket, intervalle de reconnexion).
|
|
* @param state État de la connexion du processus enfant courant.
|
|
* @param s Enregistrement du serveur Apache pour la journalisation des erreurs.
|
|
* @return 0 en cas de succès ou connexion déjà en cours, -1 en cas d'échec.
|
|
*/
|
|
static int try_connect(reqin_log_config_t *cfg, reqin_log_child_state_t *state, server_rec *s)
|
|
{
|
|
apr_time_t now;
|
|
apr_time_t reconnect_interval;
|
|
int err = 0;
|
|
int fd;
|
|
int flags;
|
|
struct sockaddr_un addr;
|
|
int rc;
|
|
|
|
if (cfg == NULL || state == NULL || s == NULL || cfg->socket_path == NULL || cfg->socket_path[0] == '\0') {
|
|
return -1;
|
|
}
|
|
|
|
if (strlen(cfg->socket_path) >= MAX_SOCKET_PATH_LEN) {
|
|
ap_log_error(APLOG_MARK, APLOG_ERR, 0, s,
|
|
MOD_REQIN_LOG_NAME ": Unix socket path too long, cannot connect");
|
|
return -1;
|
|
}
|
|
|
|
now = apr_time_now();
|
|
reconnect_interval = apr_time_from_sec(cfg->reconnect_interval);
|
|
|
|
FD_MUTEX_LOCK(state);
|
|
|
|
if (state->connect_failed &&
|
|
(now - state->last_connect_attempt) < reconnect_interval) {
|
|
FD_MUTEX_UNLOCK(state);
|
|
return -1;
|
|
}
|
|
|
|
state->last_connect_attempt = now;
|
|
|
|
if (state->socket_fd < 0) {
|
|
state->socket_fd = socket(AF_UNIX, SOCK_DGRAM, 0);
|
|
if (state->socket_fd < 0) {
|
|
err = errno;
|
|
state->connect_failed = 1;
|
|
FD_MUTEX_UNLOCK(state);
|
|
LOG_THROTTLED(state, cfg, s, APLOG_ERR, err,
|
|
"Unix socket connect failed: cannot create socket");
|
|
return -1;
|
|
}
|
|
|
|
flags = fcntl(state->socket_fd, F_GETFL, 0);
|
|
if (flags < 0) {
|
|
flags = 0;
|
|
}
|
|
fcntl(state->socket_fd, F_SETFL, flags | O_NONBLOCK);
|
|
}
|
|
|
|
fd = state->socket_fd;
|
|
memset(&addr, 0, sizeof(addr));
|
|
addr.sun_family = AF_UNIX;
|
|
snprintf(addr.sun_path, sizeof(addr.sun_path), "%s", cfg->socket_path);
|
|
|
|
rc = connect(fd, (struct sockaddr *)&addr, sizeof(addr));
|
|
if (rc < 0) {
|
|
err = errno;
|
|
if (err == EINPROGRESS || err == EAGAIN || err == EWOULDBLOCK || err == EALREADY || err == EISCONN) {
|
|
state->connect_failed = 0;
|
|
FD_MUTEX_UNLOCK(state);
|
|
return 0;
|
|
}
|
|
|
|
close(fd);
|
|
state->socket_fd = -1;
|
|
state->connect_failed = 1;
|
|
FD_MUTEX_UNLOCK(state);
|
|
LOG_THROTTLED(state, cfg, s, APLOG_ERR, err,
|
|
"Unix socket connect failed: %s", cfg->socket_path);
|
|
return -1;
|
|
}
|
|
|
|
state->connect_failed = 0;
|
|
FD_MUTEX_UNLOCK(state);
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* @brief Vérifie l'état de la connexion et tente de se reconnecter si nécessaire.
|
|
*
|
|
* Utilise un double-check sous verrou pour éviter les tentatives superflues
|
|
* en contexte haute concurrence.
|
|
*
|
|
* @param cfg Configuration du module.
|
|
* @param state État de la connexion du processus enfant courant.
|
|
* @param s Enregistrement du serveur Apache pour la journalisation des erreurs.
|
|
* @return 0 si le socket est prêt, -1 si la connexion n'est pas disponible.
|
|
*/
|
|
static int ensure_connected(reqin_log_config_t *cfg, reqin_log_child_state_t *state, server_rec *s)
|
|
{
|
|
int connected;
|
|
|
|
/* Double-check pattern: validate config and state under lock to avoid
|
|
* unnecessary reconnect attempts under high concurrency */
|
|
FD_MUTEX_LOCK(state);
|
|
connected = (state->socket_fd >= 0 && !state->connect_failed &&
|
|
cfg != NULL && cfg->socket_path != NULL && cfg->socket_path[0] != '\0');
|
|
FD_MUTEX_UNLOCK(state);
|
|
|
|
if (connected) {
|
|
return 0;
|
|
}
|
|
|
|
return try_connect(cfg, state, s);
|
|
}
|
|
|
|
/**
|
|
* @brief Envoie un bloc de données vers le socket Unix en mode non-bloquant.
|
|
*
|
|
* En cas d'erreur de connexion (EPIPE, ECONNRESET, ENOTCONN), le socket est fermé
|
|
* et marqué pour reconnexion lors du prochain appel. L'envoi partiel est traité
|
|
* comme une erreur fatale.
|
|
*
|
|
* @param data Pointeur vers les données à envoyer.
|
|
* @param len Taille des données en octets.
|
|
* @param s Enregistrement du serveur Apache pour la journalisation des erreurs.
|
|
* @param cfg Configuration du module.
|
|
* @param state État de la connexion du processus enfant courant.
|
|
* @return 0 en cas de succès, -1 en cas d'erreur.
|
|
*/
|
|
static int write_to_socket(const char *data, apr_size_t len, server_rec *s,
|
|
reqin_log_config_t *cfg, reqin_log_child_state_t *state)
|
|
{
|
|
int fd;
|
|
ssize_t n;
|
|
|
|
if (!cfg || !state || !s || !data || len == 0) {
|
|
return -1;
|
|
}
|
|
|
|
FD_MUTEX_LOCK(state);
|
|
|
|
fd = state->socket_fd;
|
|
if (fd < 0) {
|
|
FD_MUTEX_UNLOCK(state);
|
|
return -1;
|
|
}
|
|
|
|
n = send(fd, data, len, MSG_DONTWAIT | MSG_NOSIGNAL);
|
|
if (n < 0) {
|
|
int err = errno;
|
|
int conn_lost = (err == EPIPE || err == ECONNRESET || err == ENOTCONN);
|
|
|
|
if (conn_lost) {
|
|
close(fd);
|
|
state->socket_fd = -1;
|
|
state->connect_failed = 1;
|
|
}
|
|
|
|
FD_MUTEX_UNLOCK(state);
|
|
if (conn_lost) {
|
|
LOG_THROTTLED(state, cfg, s, APLOG_ERR, err,
|
|
"Unix socket write failed: connection lost");
|
|
} else {
|
|
LOG_THROTTLED(state, cfg, s, APLOG_ERR, err,
|
|
"Unix socket write failed");
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
if ((apr_size_t)n < len) {
|
|
close(fd);
|
|
state->socket_fd = -1;
|
|
state->connect_failed = 1;
|
|
FD_MUTEX_UNLOCK(state);
|
|
return -1;
|
|
}
|
|
|
|
FD_MUTEX_UNLOCK(state);
|
|
return 0;
|
|
}
|
|
|
|
/* ====== Journalisation des requêtes ====== */
|
|
|
|
/**
|
|
* @brief Récupère la valeur d'un en-tête HTTP entrant par son nom (insensible à la casse).
|
|
*
|
|
* @param r Enregistrement de la requête Apache.
|
|
* @param name Nom de l'en-tête à rechercher.
|
|
* @return Valeur de l'en-tête, ou NULL s'il est absent.
|
|
*/
|
|
static const char *get_header(request_rec *r, const char *name)
|
|
{
|
|
const apr_table_t *headers = r->headers_in;
|
|
const apr_array_header_t *arr = apr_table_elts(headers);
|
|
const apr_table_entry_t *elts = (const apr_table_entry_t *)arr->elts;
|
|
int nelts = arr->nelts;
|
|
|
|
for (int i = 0; i < nelts; i++) {
|
|
if (elts[i].key != NULL && strcasecmp(elts[i].key, name) == 0) {
|
|
return elts[i].val;
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
/**
|
|
* @brief Sérialise une requête HTTP entrante en JSON et l'envoie vers le socket Unix.
|
|
*
|
|
* Construit un objet JSON contenant les métadonnées de la requête (adresses, méthode,
|
|
* chemin, version HTTP, en-têtes demandés), en respectant les limites de taille et
|
|
* en filtrant les en-têtes sensibles. La ligne JSON est terminée par un saut de ligne.
|
|
*
|
|
* @param r Enregistrement de la requête Apache.
|
|
* @param cfg Configuration du module (liste d'en-têtes, limites, chemin socket).
|
|
* @param state État de la connexion du processus enfant courant.
|
|
* @param srv_conf Configuration complète du serveur (inclut le niveau de log).
|
|
*/
|
|
static void log_request(request_rec *r, reqin_log_config_t *cfg, reqin_log_child_state_t *state, reqin_log_server_conf_t *srv_conf)
|
|
{
|
|
apr_pool_t *pool;
|
|
server_rec *s;
|
|
dynbuf_t buf;
|
|
char port_buf[16];
|
|
const char *src_ip;
|
|
const char *dst_ip;
|
|
const char *method;
|
|
const char *path;
|
|
const char *host;
|
|
const char *http_version;
|
|
const char *scheme;
|
|
const char *query;
|
|
|
|
if (!r || !r->server || !r->pool || !r->connection) {
|
|
return;
|
|
}
|
|
|
|
pool = r->pool;
|
|
s = r->server;
|
|
|
|
if (ensure_connected(cfg, state, s) < 0) {
|
|
return;
|
|
}
|
|
|
|
src_ip = r->useragent_ip ? r->useragent_ip :
|
|
(r->connection->client_ip ? r->connection->client_ip : "");
|
|
dst_ip = r->connection->local_ip ? r->connection->local_ip : "";
|
|
method = r->method ? r->method : "UNKNOWN";
|
|
path = r->parsed_uri.path ? r->parsed_uri.path : "/";
|
|
/* Sanitize method and path to prevent log injection via oversized values */
|
|
if (strlen(method) > 32) {
|
|
method = apr_pstrmemdup(pool, method, 32);
|
|
}
|
|
if (strlen(path) > 2048) {
|
|
path = apr_pstrmemdup(pool, path, 2048);
|
|
}
|
|
host = apr_table_get(r->headers_in, "Host");
|
|
if (host == NULL) {
|
|
host = "";
|
|
} else {
|
|
/* Sanitize Host header to prevent log injection via oversized values */
|
|
if (strlen(host) > 256) {
|
|
host = apr_pstrmemdup(pool, host, 256);
|
|
}
|
|
}
|
|
http_version = r->protocol ? r->protocol : "UNKNOWN";
|
|
/* Sanitize HTTP version string */
|
|
if (strlen(http_version) > 16) {
|
|
http_version = apr_pstrmemdup(pool, http_version, 16);
|
|
}
|
|
|
|
/* scheme (https or http) */
|
|
scheme = ap_http_scheme(r);
|
|
if (scheme == NULL) {
|
|
scheme = "http";
|
|
}
|
|
|
|
/* query (query string from parsed URI, e.g., ?foo=bar) */
|
|
query = r->parsed_uri.query ? r->parsed_uri.query : "";
|
|
/* Sanitize query to prevent oversized values */
|
|
if (strlen(query) > 2048) {
|
|
query = apr_pstrmemdup(pool, query, 2048);
|
|
}
|
|
|
|
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 (nanoseconds since epoch, from request reception time) */
|
|
{
|
|
apr_uint64_t ns = ((apr_uint64_t)r->request_time) * APR_UINT64_C(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);
|
|
}
|
|
|
|
/* scheme */
|
|
dynbuf_append(&buf, "\"scheme\":\"", 10);
|
|
append_json_string(&buf, scheme);
|
|
dynbuf_append(&buf, "\",", 2);
|
|
|
|
/* src_ip */
|
|
dynbuf_append(&buf, "\"src_ip\":\"", 10);
|
|
append_json_string(&buf, src_ip);
|
|
dynbuf_append(&buf, "\",", 2);
|
|
|
|
/* src_port */
|
|
if (r->connection->client_addr != NULL) {
|
|
snprintf(port_buf, sizeof(port_buf), "%u", (unsigned int)r->connection->client_addr->port);
|
|
} else {
|
|
snprintf(port_buf, sizeof(port_buf), "0");
|
|
}
|
|
dynbuf_append(&buf, "\"src_port\":", 11);
|
|
dynbuf_append(&buf, port_buf, -1);
|
|
dynbuf_append(&buf, ",", 1);
|
|
|
|
/* dst_ip */
|
|
dynbuf_append(&buf, "\"dst_ip\":\"", 10);
|
|
append_json_string(&buf, dst_ip);
|
|
dynbuf_append(&buf, "\",", 2);
|
|
|
|
/* dst_port */
|
|
if (r->connection->local_addr != NULL) {
|
|
snprintf(port_buf, sizeof(port_buf), "%u", (unsigned int)r->connection->local_addr->port);
|
|
} else {
|
|
snprintf(port_buf, sizeof(port_buf), "0");
|
|
}
|
|
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, method);
|
|
dynbuf_append(&buf, "\",", 2);
|
|
|
|
/* path */
|
|
dynbuf_append(&buf, "\"path\":\"", 8);
|
|
append_json_string(&buf, path);
|
|
dynbuf_append(&buf, "\",", 2);
|
|
|
|
/* query */
|
|
dynbuf_append(&buf, "\"query\":\"", 9);
|
|
append_json_string(&buf, query);
|
|
dynbuf_append(&buf, "\",", 2);
|
|
|
|
/* host */
|
|
dynbuf_append(&buf, "\"host\":\"", 8);
|
|
append_json_string(&buf, host);
|
|
dynbuf_append(&buf, "\",", 2);
|
|
|
|
/* http_version */
|
|
dynbuf_append(&buf, "\"http_version\":\"", 16);
|
|
append_json_string(&buf, http_version);
|
|
dynbuf_append(&buf, "\",", 2);
|
|
|
|
/* keepalives */
|
|
dynbuf_append(&buf, "\"keepalives\":", 13);
|
|
{
|
|
char ka_buf[16];
|
|
snprintf(ka_buf, sizeof(ka_buf), "%d", r->connection->keepalives);
|
|
dynbuf_append(&buf, ka_buf, -1);
|
|
}
|
|
|
|
/* client_headers - ordered list of all header names as received from the client,
|
|
* preserving original order and case */
|
|
{
|
|
const apr_array_header_t *arr = apr_table_elts(r->headers_in);
|
|
const apr_table_entry_t *elts = (const apr_table_entry_t *)arr->elts;
|
|
int first = 1;
|
|
|
|
dynbuf_append(&buf, ",\"client_headers\":[", 19);
|
|
for (int i = 0; i < arr->nelts; i++) {
|
|
if (elts[i].key != NULL) {
|
|
if (!first) {
|
|
dynbuf_append(&buf, ",", 1);
|
|
}
|
|
dynbuf_append(&buf, "\"", 1);
|
|
append_json_string(&buf, elts[i].key);
|
|
dynbuf_append(&buf, "\"", 1);
|
|
first = 0;
|
|
}
|
|
}
|
|
dynbuf_append(&buf, "]", 1);
|
|
}
|
|
|
|
/* Check buffer size before adding headers to prevent memory exhaustion */
|
|
if (buf.len >= MAX_JSON_SIZE) {
|
|
if (SHOULD_LOG(srv_conf, REQIN_LOG_LEVEL_DEBUG)) {
|
|
ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, s,
|
|
MOD_REQIN_LOG_NAME ": JSON buffer size limit reached before headers");
|
|
}
|
|
return;
|
|
}
|
|
|
|
/* 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;
|
|
|
|
/* Skip sensitive headers to prevent credential leakage */
|
|
if (is_sensitive_header(header_name)) {
|
|
if (SHOULD_LOG(srv_conf, REQIN_LOG_LEVEL_DEBUG)) {
|
|
ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, s,
|
|
MOD_REQIN_LOG_NAME ": Skipping sensitive header: %s", header_name);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
/* Count against the limit regardless of header presence in the request */
|
|
header_count++;
|
|
|
|
header_value = get_header(r, header_name);
|
|
|
|
if (header_value != NULL) {
|
|
apr_size_t header_contrib = 9 + strlen(header_name) + 3 + strlen(header_value) + 1;
|
|
apr_size_t val_len;
|
|
char *truncated;
|
|
|
|
if (buf.len + header_contrib >= MAX_JSON_SIZE) {
|
|
if (SHOULD_LOG(srv_conf, REQIN_LOG_LEVEL_DEBUG)) {
|
|
ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, s,
|
|
MOD_REQIN_LOG_NAME ": JSON size limit reached, truncating headers");
|
|
}
|
|
break;
|
|
}
|
|
|
|
dynbuf_append(&buf, ",\"header_", 9);
|
|
append_json_string(&buf, header_name);
|
|
dynbuf_append(&buf, "\":\"", 3);
|
|
|
|
val_len = strlen(header_value);
|
|
if ((int)val_len > cfg->max_header_value_len) {
|
|
val_len = (apr_size_t)cfg->max_header_value_len;
|
|
}
|
|
|
|
truncated = apr_pstrmemdup(pool, header_value, val_len);
|
|
append_json_string(&buf, truncated);
|
|
dynbuf_append(&buf, "\"", 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
dynbuf_append(&buf, "}\n", 2);
|
|
|
|
if (buf.len > MAX_JSON_SIZE) {
|
|
apr_time_t now = apr_time_now();
|
|
apr_time_t error_interval = apr_time_from_sec(cfg->error_report_interval);
|
|
int should_report = 0;
|
|
|
|
FD_MUTEX_LOCK(state);
|
|
if ((now - state->last_error_report) >= error_interval) {
|
|
state->last_error_report = now;
|
|
should_report = 1;
|
|
}
|
|
FD_MUTEX_UNLOCK(state);
|
|
|
|
if (should_report) {
|
|
ap_log_error(APLOG_MARK, APLOG_ERR, 0, s,
|
|
MOD_REQIN_LOG_NAME ": JSON log line exceeds maximum size, dropping");
|
|
}
|
|
return;
|
|
}
|
|
|
|
write_to_socket(buf.data, buf.len, s, cfg, state);
|
|
}
|
|
|
|
/* ====== Hooks Apache ====== */
|
|
|
|
/**
|
|
* @brief Hook post_read_request — journalise la requête entrante dès sa réception.
|
|
*
|
|
* Les sous-requêtes et les redirections internes sont ignorées afin de ne
|
|
* journaliser que la requête originale du client.
|
|
*
|
|
* @param r Enregistrement de la requête Apache.
|
|
* @return DECLINED dans tous les cas (traitement non exclusif).
|
|
*/
|
|
static int reqin_log_post_read_request(request_rec *r)
|
|
{
|
|
reqin_log_server_conf_t *srv_conf = get_server_conf(r->server);
|
|
|
|
/* Skip subrequests and internal redirects to log only the original hit */
|
|
if (r->main != NULL || r->prev != NULL) {
|
|
return DECLINED;
|
|
}
|
|
|
|
if (srv_conf == NULL || srv_conf->config == NULL ||
|
|
!srv_conf->config->enabled || srv_conf->config->socket_path == NULL) {
|
|
return DECLINED;
|
|
}
|
|
|
|
log_request(r, srv_conf->config, &srv_conf->child_state, srv_conf);
|
|
return DECLINED;
|
|
}
|
|
|
|
/**
|
|
* @brief Hook child_init — initialise l'état du processus enfant et établit la connexion socket.
|
|
*
|
|
* Réinitialise le descripteur de socket, crée le mutex de protection du FD et
|
|
* tente une première connexion au socket Unix. En MPM threadé, l'absence de mutex
|
|
* entraîne la désactivation du module pour garantir la sécurité des threads.
|
|
*
|
|
* @param p Pool APR du processus enfant.
|
|
* @param s Enregistrement du serveur Apache.
|
|
*/
|
|
static void reqin_log_child_init(apr_pool_t *p, server_rec *s)
|
|
{
|
|
reqin_log_server_conf_t *srv_conf = get_server_conf(s);
|
|
int threaded_mpm = 0;
|
|
int mpm_threads;
|
|
|
|
if (srv_conf == NULL) {
|
|
return;
|
|
}
|
|
|
|
srv_conf->child_state.socket_fd = -1;
|
|
srv_conf->child_state.last_connect_attempt = 0;
|
|
srv_conf->child_state.last_error_report = 0;
|
|
srv_conf->child_state.connect_failed = 0;
|
|
|
|
/* Detect if we're running in a threaded MPM (worker/event) */
|
|
if (ap_mpm_query(AP_MPMQ_IS_THREADED, &mpm_threads) == APR_SUCCESS &&
|
|
mpm_threads > 0) {
|
|
threaded_mpm = 1;
|
|
}
|
|
|
|
srv_conf->child_state.fd_mutex = NULL;
|
|
if (apr_thread_mutex_create(&srv_conf->child_state.fd_mutex,
|
|
APR_THREAD_MUTEX_DEFAULT, p) != APR_SUCCESS) {
|
|
srv_conf->child_state.fd_mutex = NULL;
|
|
|
|
if (threaded_mpm) {
|
|
/* Critical error: cannot ensure thread safety in worker/event MPM */
|
|
ap_log_error(APLOG_MARK, APLOG_EMERG, 0, s,
|
|
MOD_REQIN_LOG_NAME ": Failed to create mutex for thread safety. "
|
|
"Module cannot operate safely in threaded MPM. Disabling.");
|
|
/* Disable module by clearing config */
|
|
srv_conf->config->enabled = 0;
|
|
return;
|
|
} else {
|
|
/* Prefork MPM: single thread per process, mutex not strictly needed */
|
|
ap_log_error(APLOG_MARK, APLOG_WARNING, 0, s,
|
|
MOD_REQIN_LOG_NAME ": Failed to create mutex, continuing in "
|
|
"degraded mode (prefork MPM detected)");
|
|
}
|
|
}
|
|
|
|
if (srv_conf->config == NULL || !srv_conf->config->enabled ||
|
|
srv_conf->config->socket_path == NULL) {
|
|
return;
|
|
}
|
|
|
|
try_connect(srv_conf->config, &srv_conf->child_state, s);
|
|
}
|
|
|
|
/**
|
|
* @brief Hook post_config — valide la configuration de tous les serveurs virtuels.
|
|
*
|
|
* Vérifie que chaque serveur ayant le module activé dispose d'un chemin de socket
|
|
* valide et non vide. Retourne une erreur HTTP en cas de configuration invalide,
|
|
* ce qui interrompt le démarrage d'Apache.
|
|
*
|
|
* @param pconf Pool de configuration Apache (non utilisé).
|
|
* @param plog Pool de journalisation Apache (non utilisé).
|
|
* @param ptemp Pool temporaire (non utilisé).
|
|
* @param s Premier enregistrement de serveur de la chaîne.
|
|
* @return OK en cas de succès, HTTP_INTERNAL_SERVER_ERROR sinon.
|
|
*/
|
|
static int reqin_log_post_config(apr_pool_t *pconf, apr_pool_t *plog, apr_pool_t *ptemp, server_rec *s)
|
|
{
|
|
server_rec *cur;
|
|
|
|
(void)pconf;
|
|
(void)plog;
|
|
(void)ptemp;
|
|
|
|
for (cur = s; cur != NULL; cur = cur->next) {
|
|
reqin_log_server_conf_t *srv_conf = get_server_conf(cur);
|
|
reqin_log_config_t *cfg;
|
|
|
|
if (srv_conf == NULL || srv_conf->config == NULL) {
|
|
continue;
|
|
}
|
|
|
|
cfg = srv_conf->config;
|
|
if (!cfg->enabled) {
|
|
continue;
|
|
}
|
|
|
|
if (cfg->socket_path == NULL || cfg->socket_path[0] == '\0') {
|
|
ap_log_error(APLOG_MARK, APLOG_EMERG, 0, cur,
|
|
MOD_REQIN_LOG_NAME ": JsonSockLogEnabled is On but JsonSockLogSocket is missing");
|
|
return HTTP_INTERNAL_SERVER_ERROR;
|
|
}
|
|
|
|
if (strlen(cfg->socket_path) >= MAX_SOCKET_PATH_LEN) {
|
|
ap_log_error(APLOG_MARK, APLOG_EMERG, 0, cur,
|
|
MOD_REQIN_LOG_NAME ": JsonSockLogSocket path too long (max %d)", (int)MAX_SOCKET_PATH_LEN - 1);
|
|
return HTTP_INTERNAL_SERVER_ERROR;
|
|
}
|
|
}
|
|
|
|
return OK;
|
|
}
|
|
|
|
/**
|
|
* @brief Enregistre tous les hooks Apache utilisés par le module.
|
|
*
|
|
* Enregistre les hooks post_config, post_read_request et child_init
|
|
* avec une priorité APR_HOOK_MIDDLE.
|
|
*
|
|
* @param p Pool APR (non utilisé).
|
|
*/
|
|
static void reqin_log_register_hooks(apr_pool_t *p)
|
|
{
|
|
(void)p;
|
|
ap_hook_post_config(reqin_log_post_config, NULL, NULL, APR_HOOK_MIDDLE);
|
|
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);
|
|
}
|