/* 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 "util_filter.h"
#include "http_connection.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 int reqin_log_log_transaction(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);

/* Forward declarations for la capture HTTP/2 */
static int reqin_h2_process_connection(conn_rec *c, void *csd);

/* 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_ns (nanoseconds since epoch, via clock_gettime CLOCK_REALTIME) */
    {
        struct timespec ts_now;
        apr_uint64_t ns;
        char ts_buf[32];
        clock_gettime(CLOCK_REALTIME, &ts_now);
        ns = (apr_uint64_t)ts_now.tv_sec * APR_UINT64_C(1000000000)
           + (apr_uint64_t)ts_now.tv_nsec;
        snprintf(ts_buf, sizeof(ts_buf), "%" APR_UINT64_T_FMT, ns);
        dynbuf_append(&buf, "\"timestamp_ns\":", 15);
        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_string */
    dynbuf_append(&buf, "\"query_string\":\"", 16);
    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.
     * headers_raw - all headers concatenated "Name: Value\r\n" preserving order.
     * header_order_signature - FNV-1a 64-bit hash of the ordered header names. */
    {
        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;
        apr_uint64_t fnv_hash = APR_UINT64_C(14695981039346656037);
        char hash_buf[24];

        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;
                /* FNV-1a sur chaque octet du nom de header */
                for (const char *p = elts[i].key; *p; p++) {
                    fnv_hash ^= (apr_uint64_t)(unsigned char)*p;
                    fnv_hash *= APR_UINT64_C(1099511628211);
                }
                /* Séparateur entre noms */
                fnv_hash ^= (apr_uint64_t)'\n';
                fnv_hash *= APR_UINT64_C(1099511628211);
            }
        }
        dynbuf_append(&buf, "]", 1);

        /* headers_raw — en-têtes bruts dans leur ordre d'émission */
        dynbuf_append(&buf, ",\"headers_raw\":\"", 16);
        for (int i = 0; i < arr->nelts; i++) {
            if (elts[i].key != NULL) {
                append_json_string(&buf, elts[i].key);
                dynbuf_append(&buf, ": ", 2);
                append_json_string(&buf, elts[i].val ? elts[i].val : "");
                dynbuf_append(&buf, "\\r\\n", 4);
            }
        }
        dynbuf_append(&buf, "\"", 1);

        /* header_order_signature — FNV-1a 64-bit hash de l'ordre des noms */
        snprintf(hash_buf, sizeof(hash_buf), "%" APR_UINT64_T_FMT, fnv_hash);
        dynbuf_append(&buf, ",\"header_order_signature\":\"", (apr_size_t)-1);
        dynbuf_append(&buf, hash_buf, -1);
        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);
            }
        }
    }

    /* Champs HTTP/2 passif depuis les notes de connexion (vides si HTTP/1.x).
     * Pour les connexions HTTP/2, mod_http2 crée des connexions secondaires (c2)
     * par stream. Le preface H2 est stocké dans les notes de la connexion
     * primaire (c1), accessible via r->connection->master. */
    {
        conn_rec *c1 = r->connection->master ? r->connection->master : r->connection;
        const char *h2_fp  = apr_table_get(c1->notes, H2_NOTE_FINGERPRINT);
        const char *h2_set = apr_table_get(c1->notes, H2_NOTE_SETTINGS);
        const char *h2_wu  = apr_table_get(c1->notes, H2_NOTE_WUPDATE);
        const char *h2_ps  = apr_table_get(c1->notes, H2_NOTE_PSEUDO_ORDER);
        const char *h2_pri = apr_table_get(c1->notes, H2_NOTE_HAS_PRIORITY);

        if (h2_set && h2_set[0] != '\0') {
            /* Champs composites (rétrocompatibilité + fingerprint matching) */
            dynbuf_append(&buf, ",\"h2_fingerprint\":\"",  (apr_size_t)-1);
            append_json_string(&buf, h2_fp  ? h2_fp  : "");
            dynbuf_append(&buf, "\",\"h2_settings_fp\":\"", (apr_size_t)-1);
            append_json_string(&buf, h2_set);
            dynbuf_append(&buf, "\",\"h2_window_update\":", (apr_size_t)-1);
            dynbuf_append(&buf, (h2_wu && h2_wu[0]) ? h2_wu : "0", (apr_size_t)-1);
            dynbuf_append(&buf, ",\"h2_pseudo_order\":\"",  (apr_size_t)-1);
            append_json_string(&buf, h2_ps  ? h2_ps  : "");
            dynbuf_append(&buf, "\"", 1);
            dynbuf_append(&buf, ",\"h2_has_priority\":",    (apr_size_t)-1);
            dynbuf_append(&buf, (h2_pri && h2_pri[0] == '1') ? "1" : "0", 1);

            /* Champs SETTINGS individuels (RFC 9113 §6.5.2).
             * Émis uniquement si le client a envoyé le paramètre
             * (-1 / absent = non émis → le champ JSON est absent). */
            static const struct { const char *note; const char *json; } sfields[] = {
                {H2_NOTE_SET_HEADER_TABLE_SIZE,      ",\"h2_header_table_size\":"},
                {H2_NOTE_SET_ENABLE_PUSH,            ",\"h2_enable_push\":"},
                {H2_NOTE_SET_MAX_CONCURRENT_STREAMS, ",\"h2_max_concurrent_streams\":"},
                {H2_NOTE_SET_INITIAL_WINDOW_SIZE,    ",\"h2_initial_window_size\":"},
                {H2_NOTE_SET_MAX_FRAME_SIZE,         ",\"h2_max_frame_size\":"},
                {H2_NOTE_SET_MAX_HEADER_LIST_SIZE,   ",\"h2_max_header_list_size\":"},
                {H2_NOTE_SET_ENABLE_CONNECT,         ",\"h2_enable_connect_protocol\":"},
            };
            int si;
            for (si = 0; si < (int)(sizeof(sfields) / sizeof(sfields[0])); si++) {
                const char *v = apr_table_get(c1->notes, sfields[si].note);
                if (v) {
                    dynbuf_append(&buf, sfields[si].json, (apr_size_t)-1);
                    dynbuf_append(&buf, v, (apr_size_t)-1);
                }
            }
        }
    }

    /* Ne pas fermer le JSON ici — les champs de réponse (status_code,
     * response_size, duration_ms) seront ajoutés par le hook log_transaction
     * qui s'exécute après le traitement complet de la requête. */
    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;
    }

    /* Stocker le JSON partiel dans les notes de la requête pour log_transaction */
    {
        char *partial = apr_pstrmemdup(r->pool, buf.data, buf.len);
        apr_table_setn(r->notes, "reqin_partial_json", partial);
    }
}

/* ====== Fingerprinting HTTP/2 passif ====== */

/** @brief Taille du buffer de lecture spéculative pour le preface HTTP/2.
 *
 * 512 octets couvrent le magic (24) + SETTINGS (≤108) + WINDOW_UPDATE (13)
 * + premier HEADERS (≤350). Valeur conservatrice, non-destructive.
 */
#define H2_PEEK_SIZE 512

/**
 * @brief Décode un entier HPACK (RFC 7541 §5.1).
 *
 * Un entier HPACK a un préfixe N-bit dans le premier octet. Si tous les bits
 * du préfixe sont à 1, la valeur continue dans les octets suivants
 * (encodage LEB128 modifié).
 *
 * @param buf    Buffer HPACK.
 * @param len    Longueur du buffer.
 * @param prefix Nombre de bits du préfixe (1–8).
 * @param pos    Position courante (modifiée).
 * @param out    Valeur décodée en sortie.
 * @return       1 si décodage réussi, 0 si buffer insuffisant.
 */
static int hpack_int_decode(const unsigned char *buf, apr_size_t len, int prefix,
                             apr_size_t *pos, unsigned int *out)
{
    unsigned int mask = (1u << prefix) - 1u;
    unsigned int b, m;

    if (*pos >= len) return 0;
    *out = buf[(*pos)++] & mask;
    if (*out < mask) return 1;

    /* Encodage multi-octet : chaque octet contribue 7 bits */
    m = 0;
    while (*pos < len) {
        b = buf[(*pos)++];
        *out += (b & 0x7fu) << m;
        m += 7;
        if (!(b & 0x80u)) return 1;
        if (m > 28) return 0;   /* Protection contre les dépassements */
    }
    return 0;
}

/**
 * @brief Saute une chaîne HPACK (RFC 7541 §5.2).
 *
 * Une chaîne HPACK est précédée d'un entier 7-bit indiquant sa longueur
 * (bit de poids fort = indicateur de compression Huffman, ignoré ici).
 *
 * @param buf  Buffer HPACK.
 * @param len  Longueur du buffer.
 * @param pos  Position courante (modifiée pour sauter la chaîne entière).
 * @return     1 si succès, 0 si buffer insuffisant.
 */
static int hpack_skip_string(const unsigned char *buf, apr_size_t len, apr_size_t *pos)
{
    unsigned int str_len = 0;
    if (!hpack_int_decode(buf, len, 7, pos, &str_len)) return 0;
    if (*pos + str_len > len) return 0;
    *pos += str_len;
    return 1;
}

/**
 * @brief Convertit un index de la table statique HPACK en caractère de pseudo-header.
 *
 * Table statique RFC 7541 Annexe A (indices 1–7 = pseudo-headers) :
 *   1 = :authority      → 'a'
 *   2 = :method GET     → 'm'
 *   3 = :method POST    → 'm'
 *   4 = :path /         → 'p'
 *   5 = :path /index    → 'p'
 *   6 = :scheme http    → 's'
 *   7 = :scheme https   → 's'
 *
 * @param index  Index dans la table statique HPACK.
 * @return       Caractère 'a', 'm', 'p', 's', ou 0 si header régulier.
 */
static char h2_hpack_pseudo(unsigned int index)
{
    switch (index) {
        case 1:           return 'a';
        case 2: case 3:   return 'm';
        case 4: case 5:   return 'p';
        case 6: case 7:   return 's';
        default:          return 0;
    }
}

/**
 * @brief Extrait l'ordre des pseudo-headers depuis un bloc HPACK.
 *
 * Parcourt le début du bloc HPACK du premier frame HEADERS pour récupérer
 * l'ordre des pseudo-headers (:method, :authority, :scheme, :path) avant
 * d'atteindre un header régulier (index ≥ 8) ou une représentation non-indexée.
 * Résultat : chaîne CSV du type "m,a,s,p".
 *
 * @param hpack  Pointeur vers le début du bloc HPACK.
 * @param len    Longueur du bloc HPACK.
 * @param out    Buffer de sortie (capacité minimale : 8 octets).
 */
static void h2_extract_pseudo_order(const unsigned char *hpack, apr_size_t len, char *out)
{
    apr_size_t pos = 0;
    int out_pos = 0;
    int first = 1;

    while (pos < len && out_pos < 7) {
        unsigned char byte = hpack[pos];

        if (byte & 0x80u) {
            /* Représentation indexée (bit 7 = 1) : 0x80 | index */
            unsigned int idx = 0;
            if (!hpack_int_decode(hpack, len, 7, &pos, &idx)) break;
            if (idx == 0) break;

            char c = h2_hpack_pseudo(idx);
            if (!c) break;   /* Index ≥ 8 : on a atteint les headers réguliers */

            if (!first) out[out_pos++] = ',';
            out[out_pos++] = c;
            first = 0;

        } else if ((byte & 0xe0u) == 0x20u) {
            /* Mise à jour de taille de table dynamique (RFC 7541 §6.3) : ignorer */
            unsigned int sz = 0;
            if (!hpack_int_decode(hpack, len, 5, &pos, &sz)) break;

        } else {
            /* Représentation littérale → header régulier ou pseudo non-indexé */
            break;
        }
    }
    out[out_pos] = '\0';
}

/**
 * @brief Parse le preface HTTP/2 client et stocke le fingerprint dans c->notes.
 *
 * Le preface HTTP/2 (RFC 9113 §3.4) commence par 24 octets de magic string,
 * suivis d'un frame SETTINGS (type 0x04), optionnellement d'un WINDOW_UPDATE
 * (type 0x08), puis d'un frame HEADERS (type 0x01).
 *
 * Stocke dans c->notes (si la connexion est bien HTTP/2) :
 *   H2_NOTE_SETTINGS     → ex. "1:65536,2:0,4:6291456,6:262144"
 *   H2_NOTE_WUPDATE      → ex. "15663105" (0 si absent)
 *   H2_NOTE_PSEUDO_ORDER → ex. "m,a,s,p"
 *   H2_NOTE_FINGERPRINT  → fingerprint Akamai : "SETTINGS|WUPDATE|PRIO|PSEUDO"
 *
 * @param c    Connexion Apache (pool + notes).
 * @param buf  Buffer contenant les premiers octets du flux (lecture spéculative).
 * @param len  Longueur effective du buffer.
 */
static void h2_parse_preface(conn_rec *c, const char *buf, apr_size_t len)
{
    static const char H2_MAGIC[] = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n";
    const apr_size_t  MAGIC_LEN  = 24u;
    const apr_size_t  FRAME_HDR  = 9u;

    char settings_buf[256] = "";
    char wupdate_buf[16]   = "0";
    char pseudo_buf[16]    = "";
    int  has_priority      = 0;
    int  settings_pos_out  = 0;

    /* Valeurs individuelles des paramètres SETTINGS (RFC 9113 §6.5.2).
     * -1 signifie « absent du preface client » (distinction importante :
     * un paramètre absent ≠ un paramètre à 0). */
    int64_t setting_vals[9];
    int i;
    for (i = 0; i < 9; i++) setting_vals[i] = -1;

    /* Vérification du magic HTTP/2 */
    if (len < MAGIC_LEN || memcmp(buf, H2_MAGIC, MAGIC_LEN) != 0) return;

    apr_size_t pos = MAGIC_LEN;

    /* Parcours des frames du preface */
    while (pos + FRAME_HDR <= len) {
        apr_size_t frame_len = ((unsigned char)buf[pos]   << 16)
                             | ((unsigned char)buf[pos+1] <<  8)
                             |  (unsigned char)buf[pos+2];
        unsigned char  type      = (unsigned char)buf[pos+3];
        unsigned char  flags     = (unsigned char)buf[pos+4];
        uint32_t       stream_id = (((unsigned char)buf[pos+5] & 0x7fu) << 24)
                                 | ((unsigned char)buf[pos+6] << 16)
                                 | ((unsigned char)buf[pos+7] <<  8)
                                 |  (unsigned char)buf[pos+8];

        pos += FRAME_HDR;

        if (pos + frame_len > len) break;   /* Frame incomplète dans le buffer */

        if (type == 0x04u && stream_id == 0 && !(flags & 0x01u)) {
            /* ---- Frame SETTINGS (hors ACK) ---- */
            apr_size_t sp = 0;
            while (sp + 6 <= frame_len &&
                   settings_pos_out < (int)sizeof(settings_buf) - 24) {
                uint16_t id  = ((unsigned char)buf[pos + sp]     << 8)
                             |  (unsigned char)buf[pos + sp + 1];
                uint32_t val = ((unsigned char)buf[pos + sp + 2] << 24)
                             | ((unsigned char)buf[pos + sp + 3] << 16)
                             | ((unsigned char)buf[pos + sp + 4] <<  8)
                             |  (unsigned char)buf[pos + sp + 5];
                sp += 6;

                if (settings_pos_out > 0)
                    settings_buf[settings_pos_out++] = ',';
                settings_pos_out += snprintf(settings_buf + settings_pos_out,
                                             (int)sizeof(settings_buf) - settings_pos_out,
                                             "%u:%u", id, val);

                /* Stocker la valeur individuelle (IDs 1-6 et 8) */
                if (id >= 1 && id <= 6)
                    setting_vals[id] = (int64_t)val;
                else if (id == 8)
                    setting_vals[8] = (int64_t)val;
            }

        } else if (type == 0x08u && stream_id == 0) {
            /* ---- Frame WINDOW_UPDATE sur la connexion ---- */
            if (frame_len >= 4) {
                uint32_t inc = (((unsigned char)buf[pos]   & 0x7fu) << 24)
                             | ((unsigned char)buf[pos+1]  << 16)
                             | ((unsigned char)buf[pos+2]  <<  8)
                             |  (unsigned char)buf[pos+3];
                snprintf(wupdate_buf, sizeof(wupdate_buf), "%u", inc);
            }

        } else if (type == 0x01u && stream_id > 0) {
            /* ---- Premier frame HEADERS → extraire l'ordre des pseudo-headers ---- */
            apr_size_t hpack_start = 0;
            int parse_ok = 1;

            if ((flags & 0x08u) && parse_ok) {
                /* Flag PADDED : 1 octet de longueur de padding */
                if (hpack_start >= frame_len) {
                    parse_ok = 0;
                } else {
                    unsigned char pad_len = (unsigned char)buf[pos + hpack_start++];
                    if (frame_len < hpack_start + (apr_size_t)pad_len)
                        parse_ok = 0;
                    else
                        frame_len -= (apr_size_t)pad_len;
                }
            }

            if ((flags & 0x20u) && parse_ok) {
                /* Flag PRIORITY : 5 octets de priorité */
                if (hpack_start + 5u > frame_len) {
                    parse_ok = 0;
                } else {
                    hpack_start += 5u;
                    has_priority = 1;
                }
            }

            if (parse_ok && hpack_start < frame_len) {
                h2_extract_pseudo_order(
                    (const unsigned char *)(buf + pos + hpack_start),
                    frame_len - hpack_start,
                    pseudo_buf
                );
            }

            pos += frame_len;
            break;   /* HEADERS frame trouvé : parse terminé */
        }

        pos += frame_len;
    }

    /* Stocker dans les notes uniquement si une connexion HTTP/2 confirmée */
    if (settings_buf[0] == '\0') return;

    char fp[512];
    snprintf(fp, sizeof(fp), "%s|%s|%d|%s",
             settings_buf, wupdate_buf, has_priority, pseudo_buf);

    apr_table_setn(c->notes, H2_NOTE_FINGERPRINT,  apr_pstrdup(c->pool, fp));
    apr_table_setn(c->notes, H2_NOTE_SETTINGS,     apr_pstrdup(c->pool, settings_buf));
    apr_table_setn(c->notes, H2_NOTE_WUPDATE,      apr_pstrdup(c->pool, wupdate_buf));
    apr_table_setn(c->notes, H2_NOTE_PSEUDO_ORDER, apr_pstrdup(c->pool, pseudo_buf));
    apr_table_setn(c->notes, H2_NOTE_HAS_PRIORITY, has_priority ? "1" : "0");

    /* Stocker chaque paramètre SETTINGS individuel (absent = note absente) */
    static const struct { int id; const char *note; } smap[] = {
        {1, H2_NOTE_SET_HEADER_TABLE_SIZE},
        {2, H2_NOTE_SET_ENABLE_PUSH},
        {3, H2_NOTE_SET_MAX_CONCURRENT_STREAMS},
        {4, H2_NOTE_SET_INITIAL_WINDOW_SIZE},
        {5, H2_NOTE_SET_MAX_FRAME_SIZE},
        {6, H2_NOTE_SET_MAX_HEADER_LIST_SIZE},
        {8, H2_NOTE_SET_ENABLE_CONNECT},
    };
    for (i = 0; i < (int)(sizeof(smap) / sizeof(smap[0])); i++) {
        int64_t v = setting_vals[smap[i].id];
        if (v >= 0) {
            char tmp[16];
            snprintf(tmp, sizeof(tmp), "%u", (uint32_t)v);
            apr_table_setn(c->notes, smap[i].note, apr_pstrdup(c->pool, tmp));
        }
    }
}

/**
 * @brief Filtre d'entrée de connexion pour la capture passive du preface HTTP/2.
 *
 * S'injecte entre le filtre SSL (déchiffrement) et mod_http2 grâce à sa
 * priorité AP_FTYPE_CONNECTION et à l'inscription via APR_HOOK_LAST.
 *
 * Stratégie : au lieu d'une lecture spéculative séparée (qui interfère avec
 * le handshake SSL et le traitement mod_http2), ce filtre se greffe sur les
 * lectures réelles. Il laisse passer les lectures spéculatives (utilisées par
 * mod_http2 pour détecter le magic H2) sans intervenir, puis sur la première
 * lecture non-spéculative, il inspecte les données déjà lues (via
 * apr_brigade_flatten, qui copie sans consommer) pour parser le preface H2.
 *
 * @param f         Filtre courant.
 * @param bb        Brigade cible pour la lecture réelle.
 * @param mode      Mode de lecture demandé (transmis à f->next).
 * @param block     Type de blocage (transmis à f->next).
 * @param readbytes Nombre d'octets demandés.
 * @return          Statut APR de la lecture réelle.
 */
/**
 * @brief Filtre d'entrée de connexion pour la capture passive du preface HTTP/2.
 *
 * S'injecte entre le filtre SSL (déchiffrement) et mod_http2 grâce à sa
 * priorité AP_FTYPE_CONNECTION et à l'inscription via APR_HOOK_LAST.
 *
 * Stratégie : au lieu d'une lecture spéculative séparée (qui interfère avec
 * le traitement mod_http2), ce filtre se greffe sur les lectures réelles.
 * Il laisse passer les lectures spéculatives (utilisées par mod_http2 pour
 * détecter le magic H2) sans intervenir, puis sur la première lecture
 * non-spéculative, il inspecte les données déjà lues (via apr_brigade_flatten,
 * qui copie sans consommer) pour parser le preface H2.
 *
 * @param f         Filtre courant.
 * @param bb        Brigade cible pour la lecture réelle.
 * @param mode      Mode de lecture demandé (transmis à f->next).
 * @param block     Type de blocage (transmis à f->next).
 * @param readbytes Nombre d'octets demandés.
 * @return          Statut APR de la lecture réelle.
 */
/**
 * @brief Hook process_connection — capture passive du preface HTTP/2.
 *
 * S'exécute AVANT mod_http2 (APR_HOOK_FIRST) et effectue une lecture
 * spéculative non-destructive de H2_PEEK_SIZE octets sur la connexion.
 * Si le preface HTTP/2 (RFC 9113 §3.4) est détecté, parse les frames
 * SETTINGS, WINDOW_UPDATE et le premier HEADERS, puis stocke les
 * résultats dans c->notes. Retourne DECLINED pour laisser mod_http2
 * (ou le handler HTTP/1.x) prendre le relais.
 *
 * @param c    Connexion Apache.
 * @param csd  Socket descriptor (non utilisé).
 * @return     DECLINED — ne gère pas la connexion, laisse les hooks suivants.
 */
static int reqin_h2_process_connection(conn_rec *c, void *csd)
{
    (void)csd;

    apr_bucket_brigade *bb = apr_brigade_create(c->pool, c->bucket_alloc);
    apr_status_t rv = ap_get_brigade(c->input_filters, bb,
                                     AP_MODE_SPECULATIVE, APR_BLOCK_READ,
                                     H2_PEEK_SIZE);
    if (rv == APR_SUCCESS) {
        char buf[H2_PEEK_SIZE];
        apr_size_t len = sizeof(buf);
        if (apr_brigade_flatten(bb, buf, &len) == APR_SUCCESS && len >= 24) {
            h2_parse_preface(c, buf, len);
        }
    }
    apr_brigade_destroy(bb);

    return DECLINED;
}

/* ====== 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 log_transaction — complète le JSON avec les champs de réponse et envoie.
 *
 * Récupère le JSON partiel stocké par post_read_request dans r->notes,
 * ajoute status_code, response_size et duration_ms, puis envoie le JSON
 * complet via le socket Unix.
 *
 * @param r  request_rec — la requête traitée.
 * @return   DECLINED pour permettre aux autres modules de logger.
 */
static int reqin_log_log_transaction(request_rec *r)
{
    reqin_log_server_conf_t *srv_conf;
    reqin_log_config_t *cfg;
    reqin_log_child_state_t *state;
    const char *partial;
    dynbuf_t buf;
    char num_buf[32];
    apr_time_t duration_us;

    if (r->main != NULL || r->prev != NULL) {
        return DECLINED;
    }

    srv_conf = get_server_conf(r->server);
    if (srv_conf == NULL || srv_conf->config == NULL ||
        !srv_conf->config->enabled || srv_conf->config->socket_path == NULL) {
        return DECLINED;
    }

    partial = apr_table_get(r->notes, "reqin_partial_json");
    if (partial == NULL) {
        return DECLINED;
    }

    cfg = srv_conf->config;
    state = &srv_conf->child_state;

    dynbuf_init(&buf, r->pool, 4096);
    dynbuf_append(&buf, partial, -1);

    /* status_code */
    snprintf(num_buf, sizeof(num_buf), "%d", r->status);
    dynbuf_append(&buf, ",\"status_code\":", 15);
    dynbuf_append(&buf, num_buf, -1);

    /* response_size (bytes sent to client) */
    snprintf(num_buf, sizeof(num_buf), "%" APR_INT64_T_FMT, (apr_int64_t)r->bytes_sent);
    dynbuf_append(&buf, ",\"response_size\":", 17);
    dynbuf_append(&buf, num_buf, -1);

    /* duration_ms (request processing time in milliseconds) */
    duration_us = apr_time_now() - r->request_time;
    snprintf(num_buf, sizeof(num_buf), "%" APR_INT64_T_FMT, (apr_int64_t)(duration_us / 1000));
    dynbuf_append(&buf, ",\"duration_ms\":", 15);
    dynbuf_append(&buf, num_buf, -1);

    /* Fermer le JSON */
    dynbuf_append(&buf, "}\n", 2);

    if (buf.len <= MAX_JSON_SIZE) {
        write_to_socket(buf.data, buf.len, r->server, cfg, state);
    }

    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;
    /* Hook process_connection AVANT mod_http2 pour capturer le preface H2 */
    ap_hook_process_connection(reqin_h2_process_connection, NULL, NULL, APR_HOOK_FIRST);

    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_log_transaction(reqin_log_log_transaction, NULL, NULL, APR_HOOK_MIDDLE);
    ap_hook_child_init(reqin_log_child_init, NULL, NULL, APR_HOOK_MIDDLE);
}
