/* * test_h2_parsing.c — Tests unitaires du fingerprinting HTTP/2 passif. * * Les fonctions testées (hpack_int_decode, h2_extract_pseudo_order, * h2_parse_preface_buf) sont réimplimentées localement pour éviter les * dépendances Apache/APR. La logique est identique à mod_reqin_log.c. */ #include #include #include #include #include #include #include /* ====== Réimplémentation locale des fonctions H2 ====== */ static int hpack_int_decode(const unsigned char *buf, size_t len, int prefix, 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; m = 0; while (*pos < len) { b = buf[(*pos)++]; *out += (b & 0x7fu) << m; m += 7; if (!(b & 0x80u)) return 1; if (m > 28) return 0; } return 0; } 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; } } static void h2_extract_pseudo_order(const unsigned char *hpack, size_t len, char *out) { 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) { 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; if (!first) out[out_pos++] = ','; out[out_pos++] = c; first = 0; } else if ((byte & 0xe0u) == 0x20u) { unsigned int sz = 0; if (!hpack_int_decode(hpack, len, 5, &pos, &sz)) break; } else { break; } } out[out_pos] = '\0'; } /* Résultat de h2_parse_preface_buf — version allégée (pas d'APR) */ typedef struct { char settings[256]; char wupdate[16]; char pseudo[16]; char fingerprint[512]; int has_priority; int is_h2; } h2_result_t; static void h2_parse_preface_buf(const char *buf, size_t len, h2_result_t *res) { static const char H2_MAGIC[] = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"; const size_t MAGIC_LEN = 24u; const size_t FRAME_HDR = 9u; memset(res, 0, sizeof(*res)); strcpy(res->wupdate, "0"); if (len < MAGIC_LEN || memcmp(buf, H2_MAGIC, MAGIC_LEN) != 0) return; int settings_out = 0; size_t pos = MAGIC_LEN; while (pos + FRAME_HDR <= len) { 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; if (type == 0x04u && stream_id == 0 && !(flags & 0x01u)) { size_t sp = 0; while (sp + 6 <= frame_len && settings_out < (int)sizeof(res->settings) - 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_out > 0) res->settings[settings_out++] = ','; settings_out += snprintf(res->settings + settings_out, (int)sizeof(res->settings) - settings_out, "%u:%u", id, val); } } else if (type == 0x08u && stream_id == 0) { 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(res->wupdate, sizeof(res->wupdate), "%u", inc); } } else if (type == 0x01u && stream_id > 0) { size_t hpack_start = 0; int parse_ok = 1; if ((flags & 0x08u) && parse_ok) { if (hpack_start >= frame_len) { parse_ok = 0; } else { unsigned char pad_len = (unsigned char)buf[pos + hpack_start++]; if (frame_len < hpack_start + (size_t)pad_len) parse_ok = 0; else frame_len -= (size_t)pad_len; } } if ((flags & 0x20u) && parse_ok) { if (hpack_start + 5u > frame_len) { parse_ok = 0; } else { hpack_start += 5u; res->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, res->pseudo ); } pos += frame_len; break; } pos += frame_len; } if (res->settings[0] != '\0') { res->is_h2 = 1; snprintf(res->fingerprint, sizeof(res->fingerprint), "%s|%s|%d|%s", res->settings, res->wupdate, res->has_priority, res->pseudo); } } /* ====== Données de test : preface Chrome 120 ====== */ /* * Preface HTTP/2 Chrome 120 (capturée) : * Magic (24 octets) * SETTINGS frame : HEADER_TABLE_SIZE=65536, ENABLE_PUSH=0, * INITIAL_WINDOW_SIZE=6291456, MAX_HEADER_LIST_SIZE=262144 * WINDOW_UPDATE : incrément 15663105 * HEADERS stream 1 : :method GET, :authority, :scheme https, :path / * → ordre HPACK indexé : 0x82(GET), 0x81(:auth), 0x87(https), 0x84(/) */ static const unsigned char CHROME_PREFACE[] = { /* Magic */ 'P','R','I',' ','*',' ','H','T','T','P','/','2','.','0','\r','\n', '\r','\n','S','M','\r','\n','\r','\n', /* SETTINGS frame : length=24, type=0x04, flags=0x00, stream=0 */ 0x00, 0x00, 0x18, /* length = 24 = 4×6 */ 0x04, /* type SETTINGS */ 0x00, /* flags = 0 */ 0x00, 0x00, 0x00, 0x00, /* stream 0 */ /* Entry 1: HEADER_TABLE_SIZE (1) = 65536 = 0x00010000 */ 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, /* Entry 2: ENABLE_PUSH (2) = 0 */ 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, /* Entry 3: INITIAL_WINDOW_SIZE (4) = 6291456 = 0x00600000 */ 0x00, 0x04, 0x00, 0x60, 0x00, 0x00, /* Entry 4: MAX_HEADER_LIST_SIZE (6) = 262144 = 0x00040000 */ 0x00, 0x06, 0x00, 0x04, 0x00, 0x00, /* WINDOW_UPDATE frame : length=4, type=0x08, flags=0, stream=0 */ 0x00, 0x00, 0x04, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, /* increment = 15663105 = 0x00EF0001 */ 0x00, 0xEF, 0x00, 0x01, /* HEADERS frame : length=14, type=0x01, flags=0x05 (END_STREAM|END_HEADERS), stream=1 */ 0x00, 0x00, 0x0E, 0x01, 0x05, 0x00, 0x00, 0x00, 0x01, /* HPACK : :method GET (0x82), :authority (0x81), :scheme https (0x87), :path / (0x84) */ /* → ordre Chrome : m,a,s,p */ 0x82, 0x81, 0x87, 0x84, /* + quelques headers supplémentaires (indices statiques) */ 0x86, /* :scheme http (index 6, régulier → stop après pseudo) */ 0x53, /* accept (sans valeur — littéral, arrête le scan) */ 0x00, 0x05, 0x74, 0x65, 0x78, 0x74, 0x2F, 0x68, 0x74, 0x6D, 0x6C }; /* ====== Données de test : preface Firefox 120 ====== */ /* * Preface HTTP/2 Firefox 120 : * SETTINGS: HEADER_TABLE_SIZE=65536, INITIAL_WINDOW_SIZE=131072, MAX_FRAME_SIZE=16384 * WINDOW_UPDATE: 12517377 * HEADERS: :method GET (0x82), :path / (0x84), :scheme https (0x87), :authority (0x81) * → ordre Firefox : m,p,s,a */ static const unsigned char FIREFOX_PREFACE[] = { /* Magic */ 'P','R','I',' ','*',' ','H','T','T','P','/','2','.','0','\r','\n', '\r','\n','S','M','\r','\n','\r','\n', /* SETTINGS frame : length=18, type=0x04, flags=0x00, stream=0 */ 0x00, 0x00, 0x12, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, /* HEADER_TABLE_SIZE (1) = 65536 */ 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, /* INITIAL_WINDOW_SIZE (4) = 131072 = 0x00020000 */ 0x00, 0x04, 0x00, 0x02, 0x00, 0x00, /* MAX_FRAME_SIZE (5) = 16384 = 0x00004000 */ 0x00, 0x05, 0x00, 0x00, 0x40, 0x00, /* WINDOW_UPDATE : increment = 12517377 = 0x00BF0001 */ 0x00, 0x00, 0x04, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xBF, 0x00, 0x01, /* HEADERS frame : length=4, type=0x01, flags=0x05, stream=1 */ 0x00, 0x00, 0x04, 0x01, 0x05, 0x00, 0x00, 0x00, 0x01, /* HPACK : :method GET (0x82), :path / (0x84), :scheme https (0x87), :authority (0x81) */ /* → ordre Firefox : m,p,s,a */ 0x82, 0x84, 0x87, 0x81 }; /* ====== Données de test : flux HTTP/1.1 (ne doit pas matcher) ====== */ static const char HTTP1_DATA[] = "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n"; /* ====== Tests ====== */ static void test_chrome_settings_parsed(void **state) { (void)state; h2_result_t res; h2_parse_preface_buf((const char *)CHROME_PREFACE, sizeof(CHROME_PREFACE), &res); assert_int_equal(res.is_h2, 1); /* SETTINGS attendus : 1:65536,2:0,4:6291456,6:262144 */ assert_string_equal(res.settings, "1:65536,2:0,4:6291456,6:262144"); } static void test_chrome_window_update(void **state) { (void)state; h2_result_t res; h2_parse_preface_buf((const char *)CHROME_PREFACE, sizeof(CHROME_PREFACE), &res); assert_string_equal(res.wupdate, "15663105"); } static void test_chrome_pseudo_order(void **state) { (void)state; h2_result_t res; h2_parse_preface_buf((const char *)CHROME_PREFACE, sizeof(CHROME_PREFACE), &res); /* Chrome : :method(m), :authority(a), :scheme(s), :path(p) */ assert_string_equal(res.pseudo, "m,a,s,p"); } static void test_chrome_fingerprint_akamai(void **state) { (void)state; h2_result_t res; h2_parse_preface_buf((const char *)CHROME_PREFACE, sizeof(CHROME_PREFACE), &res); assert_string_equal(res.fingerprint, "1:65536,2:0,4:6291456,6:262144|15663105|0|m,a,s,p"); } static void test_firefox_settings_parsed(void **state) { (void)state; h2_result_t res; h2_parse_preface_buf((const char *)FIREFOX_PREFACE, sizeof(FIREFOX_PREFACE), &res); assert_int_equal(res.is_h2, 1); assert_string_equal(res.settings, "1:65536,4:131072,5:16384"); } static void test_firefox_pseudo_order(void **state) { (void)state; h2_result_t res; h2_parse_preface_buf((const char *)FIREFOX_PREFACE, sizeof(FIREFOX_PREFACE), &res); /* Firefox : :method(m), :path(p), :scheme(s), :authority(a) */ assert_string_equal(res.pseudo, "m,p,s,a"); } static void test_firefox_fingerprint_akamai(void **state) { (void)state; h2_result_t res; h2_parse_preface_buf((const char *)FIREFOX_PREFACE, sizeof(FIREFOX_PREFACE), &res); assert_string_equal(res.fingerprint, "1:65536,4:131072,5:16384|12517377|0|m,p,s,a"); } static void test_http1_not_detected(void **state) { (void)state; h2_result_t res; h2_parse_preface_buf(HTTP1_DATA, strlen(HTTP1_DATA), &res); assert_int_equal(res.is_h2, 0); assert_string_equal(res.settings, ""); assert_string_equal(res.fingerprint, ""); } static void test_empty_buffer_not_detected(void **state) { (void)state; h2_result_t res; h2_parse_preface_buf("", 0, &res); assert_int_equal(res.is_h2, 0); } static void test_truncated_preface_no_crash(void **state) { (void)state; h2_result_t res; /* Magic complet mais frame tronquée */ h2_parse_preface_buf((const char *)CHROME_PREFACE, 30, &res); assert_int_equal(res.is_h2, 0); /* SETTINGS incomplet → pas de fingerprint */ } static void test_hpack_int_single_byte(void **state) { (void)state; /* Entier 7-bit < 127 → encodé sur 1 octet */ unsigned char buf[] = { 0x82 }; /* 0x80 | 2 → index=2 */ size_t pos = 0; unsigned int out = 0; int ok = hpack_int_decode(buf, 1, 7, &pos, &out); assert_int_equal(ok, 1); assert_int_equal(out, 2); assert_int_equal(pos, 1); } static void test_hpack_pseudo_table(void **state) { (void)state; assert_int_equal(h2_hpack_pseudo(1), 'a'); assert_int_equal(h2_hpack_pseudo(2), 'm'); assert_int_equal(h2_hpack_pseudo(3), 'm'); assert_int_equal(h2_hpack_pseudo(4), 'p'); assert_int_equal(h2_hpack_pseudo(5), 'p'); assert_int_equal(h2_hpack_pseudo(6), 's'); assert_int_equal(h2_hpack_pseudo(7), 's'); assert_int_equal(h2_hpack_pseudo(8), 0); /* header régulier */ assert_int_equal(h2_hpack_pseudo(62), 0); } static void test_pseudo_order_extraction_direct(void **state) { (void)state; /* HPACK block : :method(0x82), :path(0x84), :scheme(0x87), :authority(0x81) */ unsigned char hpack[] = { 0x82, 0x84, 0x87, 0x81 }; char out[16]; h2_extract_pseudo_order(hpack, sizeof(hpack), out); assert_string_equal(out, "m,p,s,a"); } static void test_pseudo_order_stops_at_regular_header(void **state) { (void)state; /* :method(0x82), puis header régulier (0x88 = index 8) */ unsigned char hpack[] = { 0x82, 0x88 }; char out[16]; h2_extract_pseudo_order(hpack, sizeof(hpack), out); assert_string_equal(out, "m"); } /* ====== main ====== */ int main(void) { const struct CMUnitTest tests[] = { cmocka_unit_test(test_chrome_settings_parsed), cmocka_unit_test(test_chrome_window_update), cmocka_unit_test(test_chrome_pseudo_order), cmocka_unit_test(test_chrome_fingerprint_akamai), cmocka_unit_test(test_firefox_settings_parsed), cmocka_unit_test(test_firefox_pseudo_order), cmocka_unit_test(test_firefox_fingerprint_akamai), cmocka_unit_test(test_http1_not_detected), cmocka_unit_test(test_empty_buffer_not_detected), cmocka_unit_test(test_truncated_preface_no_crash), cmocka_unit_test(test_hpack_int_single_byte), cmocka_unit_test(test_hpack_pseudo_table), cmocka_unit_test(test_pseudo_order_extraction_direct), cmocka_unit_test(test_pseudo_order_stops_at_regular_header), }; return cmocka_run_group_tests(tests, NULL, NULL); }