feat: HTTP/2 passive fingerprinting with individual SETTINGS fields
Complete implementation of HTTP/2 passive fingerprinting per thesis §2.5.3: mod-reqin-log (C module): - Replace connection-level filter with ap_hook_process_connection (APR_HOOK_FIRST) to capture H2 preface before mod_http2 takes over the connection - AP_MODE_SPECULATIVE read of 512 bytes from c->input_filters - Parse SETTINGS, WINDOW_UPDATE, PRIORITY flags, pseudo-header order - Output individual SETTINGS params as separate JSON fields (IDs 1-6, 8) - Read H2 notes from c1 (master connection) for mod_http2 secondary conns - Fix header_order_signature JSON length bug (26→strlen) ClickHouse schema: - Add 8 new columns to http_logs: h2_has_priority, h2_header_table_size, h2_enable_push, h2_max_concurrent_streams, h2_initial_window_size, h2_max_frame_size, h2_max_header_list_size, h2_enable_connect_protocol - Use Int32/Int64 with DEFAULT -1 to distinguish absent vs zero - Update mv_http_logs to extract individual fields via JSONHas/JSONExtractInt - Migration 04_http2_fields.sql updated for existing deployments Correlator: - Accept both timestamp_ns and timestamp field names (backward compat) Integration: - Enable HTTP/2 in Apache: Protocols h2 http/1.1 in httpd-integration.conf Validated end-to-end via Playwright: H2 curl traffic → mod-reqin-log → correlator → ClickHouse with all 12 H2 columns populated correctly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@ -130,15 +130,13 @@ static const char *cmd_set_log_level(cmd_parms *cmd, void *dummy, const char *ar
|
||||
|
||||
/* 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 le filtre HTTP/2 */
|
||||
static apr_status_t reqin_h2_filter(ap_filter_t *f, apr_bucket_brigade *bb,
|
||||
ap_input_mode_t mode, apr_read_type_e block,
|
||||
apr_off_t readbytes);
|
||||
static void reqin_h2_add_filter(conn_rec *c, void *csd);
|
||||
/* 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[] = {
|
||||
@ -934,12 +932,16 @@ static void log_request(request_rec *r, reqin_log_config_t *cfg, reqin_log_child
|
||||
format_iso8601(&buf, r->request_time);
|
||||
dynbuf_append(&buf, "\",", 2);
|
||||
|
||||
/* timestamp (nanoseconds since epoch, from request reception time) */
|
||||
/* timestamp_ns (nanoseconds since epoch, via clock_gettime CLOCK_REALTIME) */
|
||||
{
|
||||
apr_uint64_t ns = ((apr_uint64_t)r->request_time) * APR_UINT64_C(1000);
|
||||
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\":", 12);
|
||||
dynbuf_append(&buf, "\"timestamp_ns\":", 15);
|
||||
dynbuf_append(&buf, ts_buf, -1);
|
||||
dynbuf_append(&buf, ",", 1);
|
||||
}
|
||||
@ -989,8 +991,8 @@ static void log_request(request_rec *r, reqin_log_config_t *cfg, reqin_log_child
|
||||
append_json_string(&buf, path);
|
||||
dynbuf_append(&buf, "\",", 2);
|
||||
|
||||
/* query */
|
||||
dynbuf_append(&buf, "\"query\":\"", 9);
|
||||
/* query_string */
|
||||
dynbuf_append(&buf, "\"query_string\":\"", 16);
|
||||
append_json_string(&buf, query);
|
||||
dynbuf_append(&buf, "\",", 2);
|
||||
|
||||
@ -1013,11 +1015,15 @@ static void log_request(request_rec *r, reqin_log_config_t *cfg, reqin_log_child
|
||||
}
|
||||
|
||||
/* client_headers - ordered list of all header names as received from the client,
|
||||
* preserving original order and case */
|
||||
* 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++) {
|
||||
@ -1029,9 +1035,35 @@ static void log_request(request_rec *r, reqin_log_config_t *cfg, reqin_log_child
|
||||
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 */
|
||||
@ -1096,14 +1128,20 @@ static void log_request(request_rec *r, reqin_log_config_t *cfg, reqin_log_child
|
||||
}
|
||||
}
|
||||
|
||||
/* Champs HTTP/2 passif depuis les notes de connexion (vides si HTTP/1.x) */
|
||||
/* 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. */
|
||||
{
|
||||
const char *h2_fp = apr_table_get(r->connection->notes, H2_NOTE_FINGERPRINT);
|
||||
const char *h2_set = apr_table_get(r->connection->notes, H2_NOTE_SETTINGS);
|
||||
const char *h2_wu = apr_table_get(r->connection->notes, H2_NOTE_WUPDATE);
|
||||
const char *h2_ps = apr_table_get(r->connection->notes, H2_NOTE_PSEUDO_ORDER);
|
||||
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);
|
||||
@ -1113,11 +1151,35 @@ static void log_request(request_rec *r, reqin_log_config_t *cfg, reqin_log_child
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dynbuf_append(&buf, "}\n", 2);
|
||||
|
||||
/* 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);
|
||||
@ -1137,7 +1199,11 @@ static void log_request(request_rec *r, reqin_log_config_t *cfg, reqin_log_child
|
||||
return;
|
||||
}
|
||||
|
||||
write_to_socket(buf.data, buf.len, s, cfg, state);
|
||||
/* 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 ====== */
|
||||
@ -1307,6 +1373,13 @@ static void h2_parse_preface(conn_rec *c, const char *buf, apr_size_t len)
|
||||
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;
|
||||
|
||||
@ -1346,6 +1419,12 @@ static void h2_parse_preface(conn_rec *c, const char *buf, apr_size_t len)
|
||||
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) {
|
||||
@ -1412,6 +1491,26 @@ static void h2_parse_preface(conn_rec *c, const char *buf, apr_size_t len)
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1419,9 +1518,13 @@ static void h2_parse_preface(conn_rec *c, const char *buf, apr_size_t len)
|
||||
*
|
||||
* 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.
|
||||
* À la première invocation, effectue une lecture spéculative non-destructive
|
||||
* (AP_MODE_SPECULATIVE) de H2_PEEK_SIZE octets, parse le preface HTTP/2,
|
||||
* stocke les résultats dans c->notes, puis se retire de la chaîne.
|
||||
*
|
||||
* 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.
|
||||
@ -1430,51 +1533,58 @@ static void h2_parse_preface(conn_rec *c, const char *buf, apr_size_t len)
|
||||
* @param readbytes Nombre d'octets demandés.
|
||||
* @return Statut APR de la lecture réelle.
|
||||
*/
|
||||
static apr_status_t reqin_h2_filter(ap_filter_t *f, apr_bucket_brigade *bb,
|
||||
ap_input_mode_t mode, apr_read_type_e block,
|
||||
apr_off_t readbytes)
|
||||
{
|
||||
conn_rec *c = f->c;
|
||||
|
||||
if (!apr_table_get(c->notes, H2_NOTE_PARSED)) {
|
||||
/* Lecture spéculative : ne consomme pas les données du flux */
|
||||
apr_bucket_brigade *peek = apr_brigade_create(c->pool, c->bucket_alloc);
|
||||
apr_status_t rv = ap_get_brigade(f->next, peek,
|
||||
AP_MODE_SPECULATIVE, APR_BLOCK_READ,
|
||||
H2_PEEK_SIZE);
|
||||
if (rv == APR_SUCCESS) {
|
||||
char peek_buf[H2_PEEK_SIZE];
|
||||
apr_size_t peek_len = sizeof(peek_buf);
|
||||
if (apr_brigade_flatten(peek, peek_buf, &peek_len) == APR_SUCCESS
|
||||
&& peek_len > 0) {
|
||||
h2_parse_preface(c, peek_buf, peek_len);
|
||||
}
|
||||
}
|
||||
apr_brigade_cleanup(peek);
|
||||
apr_table_setn(c->notes, H2_NOTE_PARSED, "1");
|
||||
}
|
||||
|
||||
/* Le filtre n'est nécessaire qu'une seule fois par connexion */
|
||||
ap_remove_input_filter(f);
|
||||
|
||||
return ap_get_brigade(f->next, bb, mode, block, readbytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Hook pre_connection — enregistre le filtre HTTP/2 sur chaque connexion.
|
||||
* @brief Filtre d'entrée de connexion pour la capture passive du preface HTTP/2.
|
||||
*
|
||||
* Appelé à l'établissement de chaque connexion. Inscrit reqin_h2_filter dans
|
||||
* la chaîne d'entrée avec APR_HOOK_LAST, ce qui garantit son positionnement
|
||||
* après le filtre SSL (qui s'inscrit avec APR_HOOK_MIDDLE) et donc son accès
|
||||
* au flux HTTP/2 en clair.
|
||||
* 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 void reqin_h2_add_filter(conn_rec *c, void *csd)
|
||||
static int reqin_h2_process_connection(conn_rec *c, void *csd)
|
||||
{
|
||||
(void)csd;
|
||||
ap_add_input_filter(H2_FILTER_NAME, NULL, NULL, c);
|
||||
|
||||
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 ====== */
|
||||
@ -1506,6 +1616,73 @@ static int reqin_log_post_read_request(request_rec *r)
|
||||
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.
|
||||
*
|
||||
@ -1627,11 +1804,11 @@ static int reqin_log_post_config(apr_pool_t *pconf, apr_pool_t *plog, apr_pool_t
|
||||
static void reqin_log_register_hooks(apr_pool_t *p)
|
||||
{
|
||||
(void)p;
|
||||
/* Enregistrement du filtre de connexion HTTP/2 (avant les hooks de requête) */
|
||||
ap_register_input_filter(H2_FILTER_NAME, reqin_h2_filter, NULL, AP_FTYPE_CONNECTION);
|
||||
ap_hook_pre_connection(reqin_h2_add_filter, NULL, NULL, APR_HOOK_LAST);
|
||||
/* 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);
|
||||
}
|
||||
|
||||
@ -36,14 +36,21 @@ extern module AP_MODULE_DECLARE_DATA reqin_log_module;
|
||||
|
||||
/* ====== Fingerprinting HTTP/2 passif ====== */
|
||||
|
||||
/* Nom du filtre d'entrée de connexion pour la capture du preface HTTP/2 */
|
||||
#define H2_FILTER_NAME "REQIN_H2_PEEK"
|
||||
|
||||
/* Clés des notes de connexion stockant le fingerprint HTTP/2 parsé */
|
||||
#define H2_NOTE_FINGERPRINT "reqin_h2_fp" /* Fingerprint Akamai complet */
|
||||
#define H2_NOTE_SETTINGS "reqin_h2_set" /* Entrées SETTINGS brutes */
|
||||
#define H2_NOTE_WUPDATE "reqin_h2_wu" /* Incrément WINDOW_UPDATE */
|
||||
#define H2_NOTE_PSEUDO_ORDER "reqin_h2_ps" /* Ordre pseudo-headers */
|
||||
#define H2_NOTE_HAS_PRIORITY "reqin_h2_pri" /* Flag PRIORITY présent */
|
||||
#define H2_NOTE_PARSED "reqin_h2_done" /* Marqueur "déjà parsé" */
|
||||
|
||||
/* Clés des notes pour chaque paramètre SETTINGS individuel (RFC 9113 §6.5.2) */
|
||||
#define H2_NOTE_SET_HEADER_TABLE_SIZE "reqin_h2_s1" /* ID 1 */
|
||||
#define H2_NOTE_SET_ENABLE_PUSH "reqin_h2_s2" /* ID 2 */
|
||||
#define H2_NOTE_SET_MAX_CONCURRENT_STREAMS "reqin_h2_s3" /* ID 3 */
|
||||
#define H2_NOTE_SET_INITIAL_WINDOW_SIZE "reqin_h2_s4" /* ID 4 */
|
||||
#define H2_NOTE_SET_MAX_FRAME_SIZE "reqin_h2_s5" /* ID 5 */
|
||||
#define H2_NOTE_SET_MAX_HEADER_LIST_SIZE "reqin_h2_s6" /* ID 6 */
|
||||
#define H2_NOTE_SET_ENABLE_CONNECT "reqin_h2_s8" /* ID 8 */
|
||||
|
||||
#endif /* MOD_REQIN_LOG_H */
|
||||
|
||||
Reference in New Issue
Block a user