fix(ja4ebpf): split bpf2go generate into Ja4Tc + Ja4Ssl, fix RPM systemd-rpm-macros

- Use two separate //go:generate directives (Ja4Tc for tc_capture.c, Ja4Ssl
  for uprobe_ssl.c) to avoid duplicate LICENSE symbol and multi-file clang issue
- Update loader.go to hold tcObjs/sslObjs separately with correct field names:
  UprobeSslSetFd, UprobeSslReadEntry, UretprobeSslReadExit,
  KprobeAccept4Entry, KretprobeAccept4Exit
- Add systemd-rpm-macros to all three RPM build stages (el8/el9/el10)
  so that %{_unitdir} macro resolves correctly
- RPMs now build successfully for el8, el9, el10

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
toto
2026-04-11 23:21:11 +02:00
parent a1e4c1dad5
commit 3b047b680a
155 changed files with 197011 additions and 599 deletions

View File

@ -0,0 +1,333 @@
/*
* test_config_parsing.c - Unit tests for configuration parsing
*/
#include <stdarg.h>
#include <stddef.h>
#include <setjmp.h>
#include <cmocka.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <limits.h>
/* Default configuration values */
#define DEFAULT_MAX_HEADERS 10
#define DEFAULT_MAX_HEADER_VALUE_LEN 256
#define DEFAULT_RECONNECT_INTERVAL 10
#define DEFAULT_ERROR_REPORT_INTERVAL 10
#define MAX_SOCKET_PATH_LEN 108
/* Mock configuration structure */
typedef struct {
int enabled;
const char *socket_path;
int max_headers;
int max_header_value_len;
int reconnect_interval;
int error_report_interval;
} mock_config_t;
/* Mock parsing functions */
static int parse_enabled(const char *value)
{
if (strcasecmp(value, "on") == 0 || strcmp(value, "1") == 0) {
return 1;
}
return 0;
}
static const char *parse_socket_path(const char *value)
{
if (value == NULL || strlen(value) == 0) {
return NULL;
}
if (strlen(value) >= MAX_SOCKET_PATH_LEN) {
return NULL;
}
return value;
}
static int parse_int_strict(const char *value, int *result)
{
char *endptr = NULL;
long val;
if (value == NULL || *value == '\0' || result == NULL) {
return -1;
}
errno = 0;
val = strtol(value, &endptr, 10);
if (errno != 0 || endptr == value || *endptr != '\0' || val < INT_MIN || val > INT_MAX) {
return -1;
}
*result = (int)val;
return 0;
}
static int parse_max_headers(const char *value, int *result)
{
if (parse_int_strict(value, result) != 0 || *result < 0) {
return -1;
}
return 0;
}
static int parse_interval(const char *value, int *result)
{
if (parse_int_strict(value, result) != 0 || *result < 0) {
return -1;
}
return 0;
}
static int parse_max_header_value_len(const char *value, int *result)
{
if (parse_int_strict(value, result) != 0 || *result < 1) {
return -1;
}
return 0;
}
/* Test: Parse enabled On */
static void test_parse_enabled_on(void **state)
{
(void)state;
assert_int_equal(parse_enabled("On"), 1);
assert_int_equal(parse_enabled("on"), 1);
assert_int_equal(parse_enabled("ON"), 1);
assert_int_equal(parse_enabled("1"), 1);
}
/* Test: Parse enabled Off */
static void test_parse_enabled_off(void **state)
{
(void)state;
assert_int_equal(parse_enabled("Off"), 0);
assert_int_equal(parse_enabled("off"), 0);
assert_int_equal(parse_enabled("OFF"), 0);
assert_int_equal(parse_enabled("0"), 0);
}
/* Test: Parse socket path valid */
static void test_parse_socket_path_valid(void **state)
{
(void)state;
const char *result = parse_socket_path("/var/run/logcorrelator/http.socket");
assert_string_equal(result, "/var/run/logcorrelator/http.socket");
}
/* Test: Parse socket path empty */
static void test_parse_socket_path_empty(void **state)
{
(void)state;
const char *result = parse_socket_path("");
assert_null(result);
}
/* Test: Parse socket path NULL */
static void test_parse_socket_path_null(void **state)
{
(void)state;
const char *result = parse_socket_path(NULL);
assert_null(result);
}
/* Test: Parse socket path max length valid */
static void test_parse_socket_path_max_len_valid(void **state)
{
(void)state;
char path[MAX_SOCKET_PATH_LEN];
memset(path, 'a', MAX_SOCKET_PATH_LEN - 1);
path[MAX_SOCKET_PATH_LEN - 1] = '\0';
assert_non_null(parse_socket_path(path));
}
/* Test: Parse socket path max length invalid */
static void test_parse_socket_path_max_len_invalid(void **state)
{
(void)state;
char path[MAX_SOCKET_PATH_LEN + 1];
memset(path, 'b', MAX_SOCKET_PATH_LEN);
path[MAX_SOCKET_PATH_LEN] = '\0';
assert_null(parse_socket_path(path));
}
/* Test: Parse max headers valid */
static void test_parse_max_headers_valid(void **state)
{
int result;
(void)state;
assert_int_equal(parse_max_headers("10", &result), 0);
assert_int_equal(result, 10);
assert_int_equal(parse_max_headers("0", &result), 0);
assert_int_equal(result, 0);
assert_int_equal(parse_max_headers("100", &result), 0);
assert_int_equal(result, 100);
}
/* Test: Parse max headers invalid */
static void test_parse_max_headers_invalid(void **state)
{
int result;
(void)state;
assert_int_equal(parse_max_headers("-1", &result), -1);
assert_int_equal(parse_max_headers("abc", &result), -1);
assert_int_equal(parse_max_headers("10abc", &result), -1);
}
/* Test: Parse reconnect interval valid */
static void test_parse_reconnect_interval_valid(void **state)
{
int result;
(void)state;
assert_int_equal(parse_interval("10", &result), 0);
assert_int_equal(result, 10);
assert_int_equal(parse_interval("0", &result), 0);
assert_int_equal(result, 0);
assert_int_equal(parse_interval("60", &result), 0);
assert_int_equal(result, 60);
}
/* Test: Parse reconnect interval invalid */
static void test_parse_reconnect_interval_invalid(void **state)
{
int result;
(void)state;
assert_int_equal(parse_interval("-5", &result), -1);
assert_int_equal(parse_interval("abc", &result), -1);
assert_int_equal(parse_interval("10abc", &result), -1);
}
/* Test: Parse max header value length valid */
static void test_parse_max_header_value_len_valid(void **state)
{
int result;
(void)state;
assert_int_equal(parse_max_header_value_len("1", &result), 0);
assert_int_equal(result, 1);
assert_int_equal(parse_max_header_value_len("256", &result), 0);
assert_int_equal(result, 256);
}
/* Test: Parse max header value length invalid */
static void test_parse_max_header_value_len_invalid(void **state)
{
int result;
(void)state;
assert_int_equal(parse_max_header_value_len("0", &result), -1);
assert_int_equal(parse_max_header_value_len("-1", &result), -1);
assert_int_equal(parse_max_header_value_len("10abc", &result), -1);
}
/* Test: strict numeric parsing invalid suffix for all int directives */
static void test_strict_numeric_invalid_suffix_all(void **state)
{
int result;
(void)state;
assert_int_equal(parse_max_headers("10abc", &result), -1);
assert_int_equal(parse_interval("10abc", &result), -1);
assert_int_equal(parse_max_header_value_len("10abc", &result), -1);
}
/* Test: Default configuration values */
static void test_default_config_values(void **state)
{
(void)state;
assert_int_equal(DEFAULT_MAX_HEADERS, 10);
assert_int_equal(DEFAULT_MAX_HEADER_VALUE_LEN, 256);
assert_int_equal(DEFAULT_RECONNECT_INTERVAL, 10);
assert_int_equal(DEFAULT_ERROR_REPORT_INTERVAL, 10);
}
/* Test: Configuration validation - enabled requires socket */
static void test_config_validation_enabled_requires_socket(void **state)
{
int enabled = 1;
const char *socket = "/var/run/socket";
(void)state;
assert_true(enabled == 0 || socket != NULL);
socket = NULL;
assert_false(enabled == 0 || socket != NULL);
}
/* Test: Configuration validation - enabled with empty socket is invalid */
static void test_config_validation_enabled_with_empty_socket(void **state)
{
int enabled = 1;
const char *socket = parse_socket_path("");
(void)state;
assert_false(enabled == 0 || socket != NULL);
}
/* Test: Header value length validation */
static void test_header_value_len_validation(void **state)
{
int result;
(void)state;
assert_int_equal(parse_max_header_value_len("1", &result), 0);
assert_true(result >= 1);
assert_int_equal(parse_max_header_value_len("0", &result), -1);
}
/* Test: Large but valid values */
static void test_large_valid_values(void **state)
{
int result;
(void)state;
assert_int_equal(parse_max_headers("1000000", &result), 0);
assert_int_equal(result, 1000000);
assert_int_equal(parse_interval("86400", &result), 0);
assert_int_equal(result, 86400);
}
int main(void)
{
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_parse_enabled_on),
cmocka_unit_test(test_parse_enabled_off),
cmocka_unit_test(test_parse_socket_path_valid),
cmocka_unit_test(test_parse_socket_path_empty),
cmocka_unit_test(test_parse_socket_path_null),
cmocka_unit_test(test_parse_socket_path_max_len_valid),
cmocka_unit_test(test_parse_socket_path_max_len_invalid),
cmocka_unit_test(test_parse_max_headers_valid),
cmocka_unit_test(test_parse_max_headers_invalid),
cmocka_unit_test(test_parse_reconnect_interval_valid),
cmocka_unit_test(test_parse_reconnect_interval_invalid),
cmocka_unit_test(test_parse_max_header_value_len_valid),
cmocka_unit_test(test_parse_max_header_value_len_invalid),
cmocka_unit_test(test_strict_numeric_invalid_suffix_all),
cmocka_unit_test(test_default_config_values),
cmocka_unit_test(test_config_validation_enabled_requires_socket),
cmocka_unit_test(test_config_validation_enabled_with_empty_socket),
cmocka_unit_test(test_header_value_len_validation),
cmocka_unit_test(test_large_valid_values),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}

View File

@ -0,0 +1,458 @@
/*
* 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 <stdarg.h>
#include <stddef.h>
#include <setjmp.h>
#include <cmocka.h>
#include <string.h>
#include <stdio.h>
#include <stdint.h>
/* ====== 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);
}

View File

@ -0,0 +1,226 @@
/*
* test_header_handling.c - Unit tests for header handling (truncation and limits)
*/
#include <stdarg.h>
#include <stddef.h>
#include <setjmp.h>
#include <cmocka.h>
#include <string.h>
#include <stdio.h>
#include <apr_strings.h>
#include <apr_tables.h>
#include <apr_pools.h>
#include <apr_general.h>
/* Mock header truncation function */
static char *truncate_header_value(apr_pool_t *pool, const char *value, int max_len)
{
if (value == NULL) {
return NULL;
}
size_t len = strlen(value);
if ((int)len > max_len) {
return apr_pstrmemdup(pool, value, max_len);
}
return apr_pstrdup(pool, value);
}
/* Mock header matching function */
static int header_name_matches(const char *configured, const char *actual)
{
return strcasecmp(configured, actual) == 0;
}
/* Test: Header value within limit */
static void test_header_truncation_within_limit(void **state)
{
apr_pool_t *pool;
apr_pool_create(&pool, NULL);
const char *value = "short value";
char *result = truncate_header_value(pool, value, 256);
assert_string_equal(result, "short value");
apr_pool_destroy(pool);
}
/* Test: Header value exactly at limit */
static void test_header_truncation_exact_limit(void **state)
{
apr_pool_t *pool;
apr_pool_create(&pool, NULL);
const char *value = "exactly10c";
char *result = truncate_header_value(pool, value, 10);
assert_string_equal(result, "exactly10c");
apr_pool_destroy(pool);
}
/* Test: Header value exceeds limit */
static void test_header_truncation_exceeds_limit(void **state)
{
apr_pool_t *pool;
apr_pool_create(&pool, NULL);
const char *value = "this is a very long header value that should be truncated";
char *result = truncate_header_value(pool, value, 15);
assert_string_equal(result, "this is a very ");
assert_int_equal(strlen(result), 15);
apr_pool_destroy(pool);
}
/* Test: Header value with limit of 1 */
static void test_header_truncation_limit_one(void **state)
{
apr_pool_t *pool;
apr_pool_create(&pool, NULL);
const char *value = "abc";
char *result = truncate_header_value(pool, value, 1);
assert_string_equal(result, "a");
apr_pool_destroy(pool);
}
/* Test: NULL header value */
static void test_header_truncation_null(void **state)
{
apr_pool_t *pool;
apr_pool_create(&pool, NULL);
char *result = truncate_header_value(pool, NULL, 256);
assert_null(result);
apr_pool_destroy(pool);
}
/* Test: Empty header value */
static void test_header_truncation_empty(void **state)
{
apr_pool_t *pool;
apr_pool_create(&pool, NULL);
const char *value = "";
char *result = truncate_header_value(pool, value, 256);
assert_string_equal(result, "");
apr_pool_destroy(pool);
}
/* Test: Header name matching (case-insensitive) */
static void test_header_name_matching_case_insensitive(void **state)
{
assert_true(header_name_matches("X-Request-Id", "x-request-id"));
assert_true(header_name_matches("user-agent", "User-Agent"));
assert_true(header_name_matches("HOST", "host"));
}
/* Test: Header name matching (different headers) */
static void test_header_name_matching_different(void **state)
{
assert_false(header_name_matches("X-Request-Id", "X-Trace-Id"));
assert_false(header_name_matches("Host", "User-Agent"));
}
/* Test: Multiple headers with limit */
static void test_header_count_limit(void **state)
{
apr_pool_t *pool;
apr_pool_create(&pool, NULL);
/* Simulate configured headers */
const char *configured[] = {"X-Request-Id", "X-Trace-Id", "User-Agent", "Referer"};
int configured_count = 4;
int max_headers = 2;
/* Simulate present headers */
const char *present[] = {"X-Request-Id", "User-Agent", "Referer"};
int present_count = 3;
int logged_count = 0;
for (int i = 0; i < configured_count && logged_count < max_headers; i++) {
for (int j = 0; j < present_count; j++) {
if (header_name_matches(configured[i], present[j])) {
logged_count++;
break;
}
}
}
assert_int_equal(logged_count, 2);
apr_pool_destroy(pool);
}
/* Test: Header value with special JSON characters */
static void test_header_value_json_special(void **state)
{
apr_pool_t *pool;
apr_pool_create(&pool, NULL);
const char *value = "test\"value\\with\tspecial";
char *truncated = truncate_header_value(pool, value, 256);
/* Truncation should preserve the value */
assert_string_equal(truncated, "test\"value\\with\tspecial");
apr_pool_destroy(pool);
}
/* Test: Unicode in header value (UTF-8) */
static void test_header_value_unicode(void **state)
{
apr_pool_t *pool;
apr_pool_create(&pool, NULL);
const char *value = "Mozilla/5.0 (compatible; 日本語)";
char *result = truncate_header_value(pool, value, 50);
/* Should be truncated but valid */
assert_non_null(result);
assert_true(strlen(result) <= 50);
apr_pool_destroy(pool);
}
static int group_setup(void **state)
{
(void)state;
return apr_initialize();
}
static int group_teardown(void **state)
{
(void)state;
apr_terminate();
return 0;
}
int main(void)
{
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_header_truncation_within_limit),
cmocka_unit_test(test_header_truncation_exact_limit),
cmocka_unit_test(test_header_truncation_exceeds_limit),
cmocka_unit_test(test_header_truncation_limit_one),
cmocka_unit_test(test_header_truncation_null),
cmocka_unit_test(test_header_truncation_empty),
cmocka_unit_test(test_header_name_matching_case_insensitive),
cmocka_unit_test(test_header_name_matching_different),
cmocka_unit_test(test_header_count_limit),
cmocka_unit_test(test_header_value_json_special),
cmocka_unit_test(test_header_value_unicode),
};
return cmocka_run_group_tests(tests, group_setup, group_teardown);
}

View File

@ -0,0 +1,266 @@
/*
* test_json_serialization.c - Unit tests for JSON serialization
*/
#include <stdarg.h>
#include <stddef.h>
#include <setjmp.h>
#include <cmocka.h>
#include <string.h>
#include <stdio.h>
#include <apr_pools.h>
#include <apr_strings.h>
#include <apr_time.h>
#include <apr_lib.h>
#include <apr_general.h>
typedef struct {
char *data;
size_t len;
size_t cap;
apr_pool_t *pool;
} testbuf_t;
static void testbuf_init(testbuf_t *buf, apr_pool_t *pool, size_t initial_capacity)
{
buf->pool = pool;
buf->cap = initial_capacity;
buf->len = 0;
buf->data = apr_palloc(pool, initial_capacity);
buf->data[0] = '\0';
}
static void testbuf_append(testbuf_t *buf, const char *str, size_t len)
{
if (str == NULL) {
return;
}
if (len == (size_t)-1) {
len = strlen(str);
}
if (buf->len + len + 1 > buf->cap) {
size_t new_cap = (buf->len + len + 1) * 2;
char *new_data = apr_palloc(buf->pool, new_cap);
memcpy(new_data, buf->data, buf->len + 1); /* Copy including null terminator */
buf->data = new_data;
buf->cap = new_cap;
}
memcpy(buf->data + buf->len, str, len);
buf->len += len;
buf->data[buf->len] = '\0';
}
static void testbuf_append_char(testbuf_t *buf, char c)
{
if (buf->len + 2 > buf->cap) {
size_t new_cap = (buf->cap * 2);
char *new_data = apr_palloc(buf->pool, new_cap);
memcpy(new_data, buf->data, buf->len + 1);
buf->data = new_data;
buf->cap = new_cap;
}
buf->data[buf->len++] = c;
buf->data[buf->len] = '\0';
}
/* Mock JSON string escaping function for testing */
static void append_json_string(testbuf_t *buf, const char *str)
{
if (str == NULL) {
return;
}
for (const char *p = str; *p; p++) {
char c = *p;
switch (c) {
case '"': testbuf_append(buf, "\\\"", 2); break;
case '\\': testbuf_append(buf, "\\\\", 2); break;
case '\b': testbuf_append(buf, "\\b", 2); break;
case '\f': testbuf_append(buf, "\\f", 2); break;
case '\n': testbuf_append(buf, "\\n", 2); break;
case '\r': testbuf_append(buf, "\\r", 2); break;
case '\t': testbuf_append(buf, "\\t", 2); break;
default:
if ((unsigned char)c < 0x20) {
char unicode[8];
apr_snprintf(unicode, sizeof(unicode), "\\u%04x", (unsigned char)c);
testbuf_append(buf, unicode, (size_t)-1);
} else {
testbuf_append_char(buf, c);
}
break;
}
}
}
/* Test: Empty string */
static void test_json_escape_empty_string(void **state)
{
apr_pool_t *pool;
testbuf_t buf;
(void)state;
apr_pool_create(&pool, NULL);
testbuf_init(&buf, pool, 256);
append_json_string(&buf, "");
assert_string_equal(buf.data, "");
apr_pool_destroy(pool);
}
/* Test: Simple string without special characters */
static void test_json_escape_simple_string(void **state)
{
apr_pool_t *pool;
testbuf_t buf;
(void)state;
apr_pool_create(&pool, NULL);
testbuf_init(&buf, pool, 256);
append_json_string(&buf, "hello world");
assert_string_equal(buf.data, "hello world");
apr_pool_destroy(pool);
}
/* Test: String with double quotes */
static void test_json_escape_quotes(void **state)
{
apr_pool_t *pool;
testbuf_t buf;
(void)state;
apr_pool_create(&pool, NULL);
testbuf_init(&buf, pool, 256);
append_json_string(&buf, "hello \"world\"");
assert_string_equal(buf.data, "hello \\\"world\\\"");
apr_pool_destroy(pool);
}
/* Test: String with backslashes */
static void test_json_escape_backslashes(void **state)
{
apr_pool_t *pool;
testbuf_t buf;
(void)state;
apr_pool_create(&pool, NULL);
testbuf_init(&buf, pool, 256);
append_json_string(&buf, "path\\to\\file");
assert_string_equal(buf.data, "path\\\\to\\\\file");
apr_pool_destroy(pool);
}
/* Test: String with newlines and tabs */
static void test_json_escape_newlines_tabs(void **state)
{
apr_pool_t *pool;
testbuf_t buf;
(void)state;
apr_pool_create(&pool, NULL);
testbuf_init(&buf, pool, 256);
append_json_string(&buf, "line1\nline2\ttab");
assert_string_equal(buf.data, "line1\\nline2\\ttab");
apr_pool_destroy(pool);
}
/* Test: String with control characters */
static void test_json_escape_control_chars(void **state)
{
apr_pool_t *pool;
testbuf_t buf;
(void)state;
apr_pool_create(&pool, NULL);
testbuf_init(&buf, pool, 256);
/* Test with bell character (0x07) - use octal literal */
append_json_string(&buf, "test\007bell");
/* Should contain unicode escape for bell (0x07) */
assert_true(strstr(buf.data, "\\u0007") != NULL);
apr_pool_destroy(pool);
}
/* Test: NULL string */
static void test_json_escape_null_string(void **state)
{
apr_pool_t *pool;
testbuf_t buf;
(void)state;
apr_pool_create(&pool, NULL);
testbuf_init(&buf, pool, 256);
append_json_string(&buf, NULL);
assert_string_equal(buf.data, "");
apr_pool_destroy(pool);
}
/* Test: Complex user agent string */
static void test_json_escape_user_agent(void **state)
{
apr_pool_t *pool;
testbuf_t buf;
const char *ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) \"Test\"";
(void)state;
apr_pool_create(&pool, NULL);
testbuf_init(&buf, pool, 512);
append_json_string(&buf, ua);
assert_true(strstr(buf.data, "\\\"Test\\\"") != NULL);
apr_pool_destroy(pool);
}
static int group_setup(void **state)
{
(void)state;
return apr_initialize();
}
static int group_teardown(void **state)
{
(void)state;
apr_terminate();
return 0;
}
int main(void)
{
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_json_escape_empty_string),
cmocka_unit_test(test_json_escape_simple_string),
cmocka_unit_test(test_json_escape_quotes),
cmocka_unit_test(test_json_escape_backslashes),
cmocka_unit_test(test_json_escape_newlines_tabs),
cmocka_unit_test(test_json_escape_control_chars),
cmocka_unit_test(test_json_escape_null_string),
cmocka_unit_test(test_json_escape_user_agent),
};
return cmocka_run_group_tests(tests, group_setup, group_teardown);
}

File diff suppressed because it is too large Load Diff