package parser_test import ( "testing" "github.com/antitbone/ja4/ja4ebpf/internal/parser" ) func TestDetectH2PrefaceTrue(t *testing.T) { preface := []byte("PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n") data := append(preface, 0x00, 0x00) // données supplémentaires if !parser.DetectH2Preface(data) { t.Error("H2Magic non détecté dans un buffer valide") } } func TestDetectH2PrefaceFalse(t *testing.T) { if parser.DetectH2Preface([]byte("GET / HTTP/1.1\r\n")) { t.Error("détection faux positif HTTP/1.1 comme HTTP/2") } } func TestDetectH2PrefaceTooShort(t *testing.T) { if parser.DetectH2Preface([]byte("PRI *")) { t.Error("détection sur buffer trop court") } } func TestH2MagicPrefaceLen(t *testing.T) { if parser.H2MagicPrefaceLen() != 24 { t.Errorf("longueur préambule HTTP/2 attendue 24, obtenue %d", parser.H2MagicPrefaceLen()) } } func TestParseH2ClientPrefaceSettingsEmpty(t *testing.T) { // Frame SETTINGS vide (longueur 0, aucun paramètre) sur stream 0 frame := buildH2Frame(0x4, 0x0, 0, []byte{}) settings, err := parser.ParseH2ClientPreface(frame) if err != nil { t.Fatalf("ParseH2ClientPreface: %v", err) } if settings == nil { t.Fatal("settings ne doit pas être nil") } // Tous les champs doivent être -1 (absent) if settings.HeaderTableSize != -1 { t.Errorf("HeaderTableSize: attendu -1, obtenu %d", settings.HeaderTableSize) } if settings.InitialWindowSize != -1 { t.Errorf("InitialWindowSize: attendu -1, obtenu %d", settings.InitialWindowSize) } } func TestParseH2ClientPrefaceSettingsWithValues(t *testing.T) { // Frame SETTINGS avec INITIAL_WINDOW_SIZE=65536 et MAX_CONCURRENT_STREAMS=100 settingsPayload := []byte{ 0x00, 0x04, 0x00, 0x01, 0x00, 0x00, // INITIAL_WINDOW_SIZE = 65536 0x00, 0x03, 0x00, 0x00, 0x00, 0x64, // MAX_CONCURRENT_STREAMS = 100 } frame := buildH2Frame(0x4, 0x0, 0, settingsPayload) settings, err := parser.ParseH2ClientPreface(frame) if err != nil { t.Fatalf("ParseH2ClientPreface: %v", err) } if settings.InitialWindowSize != 65536 { t.Errorf("InitialWindowSize: attendu 65536, obtenu %d", settings.InitialWindowSize) } if settings.MaxConcurrentStreams != 100 { t.Errorf("MaxConcurrentStreams: attendu 100, obtenu %d", settings.MaxConcurrentStreams) } // Les paramètres non présents restent à -1 if settings.HeaderTableSize != -1 { t.Errorf("HeaderTableSize non fourni: attendu -1, obtenu %d", settings.HeaderTableSize) } } func TestParseH2ClientPrefaceWindowUpdate(t *testing.T) { // Frame WINDOW_UPDATE sur stream 0 avec incrément = 1073741824 wuPayload := []byte{0x40, 0x00, 0x00, 0x00} // 0x40000000 = 1073741824 frame := buildH2Frame(0x8, 0x0, 0, wuPayload) settings, err := parser.ParseH2ClientPreface(frame) if err != nil { t.Fatalf("ParseH2ClientPreface: %v", err) } if settings.WindowUpdateIncrement != 1073741824 { t.Errorf("WindowUpdateIncrement: attendu 1073741824, obtenu %d", settings.WindowUpdateIncrement) } } func TestParseH2ClientPrefaceCombined(t *testing.T) { // SETTINGS + WINDOW_UPDATE combinés (comme envoyé par curl/h2) settingsPayload := []byte{ 0x00, 0x01, 0x00, 0x00, 0x10, 0x00, // HEADER_TABLE_SIZE = 4096 0x00, 0x04, 0x00, 0x00, 0xff, 0xff, // INITIAL_WINDOW_SIZE = 65535 } wuPayload := []byte{0x00, 0x0f, 0x00, 0x01} // WINDOW_UPDATE incr = 983041 frames := buildH2Frame(0x4, 0x0, 0, settingsPayload) frames = append(frames, buildH2Frame(0x8, 0x0, 0, wuPayload)...) settings, err := parser.ParseH2ClientPreface(frames) if err != nil { t.Fatalf("ParseH2ClientPreface: %v", err) } if settings.HeaderTableSize != 4096 { t.Errorf("HeaderTableSize: attendu 4096, obtenu %d", settings.HeaderTableSize) } if settings.InitialWindowSize != 65535 { t.Errorf("InitialWindowSize: attendu 65535, obtenu %d", settings.InitialWindowSize) } if settings.WindowUpdateIncrement != 983041 { t.Errorf("WindowUpdateIncrement: attendu 983041, obtenu %d", settings.WindowUpdateIncrement) } } func TestParseH2ClientPrefaceEmpty(t *testing.T) { // Données vides : doit retourner sans erreur, settings avec valeurs par défaut (-1) settings, err := parser.ParseH2ClientPreface([]byte{}) if err != nil { t.Fatalf("ParseH2ClientPreface sur vide: %v", err) } if settings == nil { t.Error("settings ne doit pas être nil même pour données vides") } if settings.HeaderTableSize != -1 { t.Errorf("HeaderTableSize: attendu -1 par défaut, obtenu %d", settings.HeaderTableSize) } } func TestParseH2ClientPrefaceTruncatedFrame(t *testing.T) { // Frame tronquée : en-tête complet mais payload incomplet truncated := []byte{0x00, 0x00, 0x06, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01} // payload tronqué settings, err := parser.ParseH2ClientPreface(truncated) if err != nil { t.Fatalf("ParseH2ClientPreface sur frame tronquée: %v (doit tolérer)", err) } // Les paramètres restent à -1 car le payload est incomplet _ = settings } // ── Helpers ─────────────────────────────────────────────────────────────── // buildH2Frame construit une frame HTTP/2 brute (en-tête 9 octets + payload). func buildH2Frame(frameType, flags uint8, streamID uint32, payload []byte) []byte { l := len(payload) frame := []byte{ byte(l >> 16), byte(l >> 8), byte(l), // longueur sur 3 octets frameType, flags, byte(streamID >> 24), byte(streamID >> 16), byte(streamID >> 8), byte(streamID), } return append(frame, payload...) } func TestDecodeH2HeadersBlockLiteralWithIndexedName(t *testing.T) { // Literal with incremental indexing, indexed name (user-agent = index 95) // Prefix byte: 0x40 | 95 = 0x5F... wait, 95 > 63 so we need multi-byte // For index 95: first byte = 0x40 | 0x3F = 0x7F, second byte = 95 - 63 = 32 = 0x20 // Then value: 7-bit length "Mozilla/5.0" = 11 bytes, no Huffman h2block := []byte{ 0x7F, 0x20, // indexed name = 95 (user-agent), with incremental indexing 0x0B, 'M', 'o', 'z', 'i', 'l', 'l', 'a', '/', '5', '.', '0', // value length 11 + value } kv, order := parser.DecodeH2HeadersBlock(h2block) if kv["user-agent"] != "Mozilla/5.0" { t.Errorf("user-agent: attendu 'Mozilla/5.0', obtenu %q", kv["user-agent"]) } if len(order) != 1 || order[0] != "user-agent" { t.Errorf("order: attendu [user-agent], obtenu %v", order) } } func TestDecodeH2HeadersBlockLiteralWithoutIndexing(t *testing.T) { // Literal without indexing, indexed name (accept-encoding = index 16) // 4-bit prefix max = 15, so index 16 needs multi-byte: 0x0F 0x01 h2block := []byte{ 0x0F, 0x01, // literal without indexing, name index = 16 (accept-encoding) 0x12, 'g', 'z', 'i', 'p', ',', ' ', 'd', 'e', 'f', 'l', 'a', 't', 'e', ',', ' ', 'b', 'r', // value } kv, _ := parser.DecodeH2HeadersBlock(h2block) if kv["accept-encoding"] != "gzip, deflate, br" { t.Errorf("accept-encoding: attendu 'gzip, deflate, br', obtenu %q", kv["accept-encoding"]) } } func TestDecodeH2HeadersBlockLiteralNewName(t *testing.T) { // Literal with incremental indexing, new name // Prefix byte: 0x40 (index = 0, new name) // Name: "x-custom-header", Value: "test-value" name := "x-custom-header" value := "test-value" h2block := []byte{ 0x40, // literal with incremental indexing, new name byte(len(name)), // name length } h2block = append(h2block, []byte(name)...) h2block = append(h2block, byte(len(value))) h2block = append(h2block, []byte(value)...) kv, order := parser.DecodeH2HeadersBlock(h2block) // x-custom-header is not in hpackCapturedHeaders, so it won't be in kv if len(kv) != 0 { t.Errorf("x-custom-header ne doit pas être capturé (pas dans hpackCapturedHeaders), obtenu %v", kv) } _ = order } func TestDecodeH2HeadersBlockPseudoHeaders(t *testing.T) { // Pseudo-headers :method GET (indexed, byte 0x82), :path / (indexed, byte 0x84) // Then :authority as literal with indexed name (index 1) // 0x40 | 1 = 0x41, then value "example.com" h2block := []byte{ 0x82, // indexed :method GET 0x84, // indexed :path / 0x41, // literal with incremental indexing, name index 1 (:authority) 0x0B, 'e', 'x', 'a', 'm', 'p', 'l', 'e', '.', 'c', 'o', 'm', // value } kv, order := parser.DecodeH2HeadersBlock(h2block) if kv[":authority"] != "example.com" { t.Errorf(":authority: attendu 'example.com', obtenu %q", kv[":authority"]) } if len(order) < 1 { t.Errorf("order ne doit pas être vide, obtenu %v", order) } } func TestIsH2FrameHeader(t *testing.T) { // Frame SETTINGS valide frame := buildH2Frame(0x4, 0x0, 0, []byte{}) if !parser.IsH2FrameHeader(frame) { t.Error("IsH2FrameHeader doit retourner true pour frame SETTINGS valide") } // Données aléatoires random := []byte{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF} if parser.IsH2FrameHeader(random) { t.Error("IsH2FrameHeader doit retourner false pour données invalides (length > 16384)") } // Trop court if parser.IsH2FrameHeader([]byte{0x00, 0x00}) { t.Error("IsH2FrameHeader doit retourner false pour données trop courtes") } } func TestExtractH2HeaderKV(t *testing.T) { // HEADERS frame with :authority literal headersPayload := []byte{ 0x41, // literal with incremental indexing, name index 1 (:authority) 0x07, 'e', 'x', 'a', 'm', 'p', 'l', 'e', // value } frame := buildH2Frame(0x1, 0x04, 1, headersPayload) // HEADERS, END_HEADERS, stream 1 kv := parser.ExtractH2HeaderKV(frame) if kv[":authority"] != "example" { t.Errorf(":authority: attendu 'example', obtenu %q", kv[":authority"]) } } func TestFormatTCPOptions(t *testing.T) { // MSS(2,4bytes) + WS(3,3bytes) + SACK(4,2bytes) + NOP(1) + TS(8,10bytes) opts := []byte{ 2, 4, 0x05, 0xB4, // MSS = 1460 3, 3, 6, // WS = 6 4, 2, // SACK Permitted 1, // NOP 8, 10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // TS } // This function is in the writer package, not parser - skip direct test here _ = opts }