Files
Jacquin Antoine f0c8fe81c6 feat(ja4ebpf): add multi-interface TC, LPM_TRIE ignore_src, unit tests, and fix bugs
- Add multi-interface TC attachment (default "any" = all UP interfaces)
- Add BPF LPM_TRIE map ignored_src for kernel-side CIDR filtering
- Add userspace ignore_src filtering for SSL/accept4 path via net.IPNet.Contains()
- Add AcceptCache for fd→SessionKey correlation with TTL and Close()
- Add 5 test files covering writer, procutil, dispatcher, accept_cache, and cmd
- Fix formatTCPOptions infinite loop on EOL (case 0 break→return)
- Fix pseudoOrderToShort panic on empty slice (negative cap)
- Fix AcceptCache goroutine leak (add done channel + Close())
- Update config.yml.example with interfaces, listen_ports, ignore_src
- Rewrite docs/services/ja4ebpf.md (was massively stale: XDP, RingBuffer, etc.)
- Fix stale XDP/RingBuffer references in docs/architecture.md, thesis, tls.go

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-16 01:49:26 +02:00

735 lines
23 KiB
Go

package parser_test
import (
"testing"
"github.com/antitbone/ja4/ja4ebpf/internal/parser"
"golang.org/x/net/http2"
)
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())
}
}
// 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...)
}
// TestH2ConnStateSettings verifies that H2ConnState processes SETTINGS frames correctly.
func TestH2ConnStateSettings(t *testing.T) {
conn := parser.NewH2ConnState()
// SETTINGS frame with HEADER_TABLE_SIZE=4096, INITIAL_WINDOW_SIZE=65535
settingsPayload := []byte{
0x00, 0x01, 0x00, 0x00, 0x10, 0x00, // HEADER_TABLE_SIZE = 4096
0x00, 0x04, 0x00, 0x00, 0xff, 0xff, // INITIAL_WINDOW_SIZE = 65535
}
frame := buildH2Frame(0x4, 0x0, 0, settingsPayload) // SETTINGS, no flags, stream 0
result, err := conn.ProcessFrames(frame, 0)
if err != nil {
t.Fatalf("ProcessFrames: %v", err)
}
if result == nil {
t.Fatal("result ne doit pas être nil")
}
if result.ClientSettings == nil {
t.Fatal("ClientSettings ne doit pas être nil")
}
if result.ClientSettings.HeaderTableSize != 4096 {
t.Errorf("HeaderTableSize: attendu 4096, obtenu %d", result.ClientSettings.HeaderTableSize)
}
if result.ClientSettings.InitialWindowSize != 65535 {
t.Errorf("InitialWindowSize: attendu 65535, obtenu %d", result.ClientSettings.InitialWindowSize)
}
}
// TestH2ConnStateWindowUpdate verifies WINDOW_UPDATE on stream 0.
func TestH2ConnStateWindowUpdate(t *testing.T) {
conn := parser.NewH2ConnState()
// WINDOW_UPDATE on stream 0 with increment = 1073741824 (0x40000000)
wuPayload := []byte{0x40, 0x00, 0x00, 0x00}
frame := buildH2Frame(0x8, 0x0, 0, wuPayload) // WINDOW_UPDATE, stream 0
result, err := conn.ProcessFrames(frame, 0)
if err != nil {
t.Fatalf("ProcessFrames: %v", err)
}
if result == nil {
t.Fatal("result ne doit pas être nil")
}
if result.ConnWindowUpdate != 1073741824 {
t.Errorf("WindowUpdateIncrement: attendu 1073741824, obtenu %d", result.ConnWindowUpdate)
}
}
// TestH2ConnStateHeadersWithHPACK verifies HEADERS frame decoding via hpack.Decoder.
func TestH2ConnStateHeadersWithHPACK(t *testing.T) {
conn := parser.NewH2ConnState()
// HEADERS frame with END_HEADERS flag:
// 0x82 = :method GET (indexed)
// 0x84 = :path / (indexed)
// 0x41 = :authority with literal value "example.com"
headersPayload := []byte{
0x82, // :method GET
0x84, // :path /
0x41, // :authority with literal value
0x0B, 'e', 'x', 'a', 'm', 'p', 'l', 'e', '.', 'c', 'o', 'm',
}
frame := buildH2Frame(0x1, 0x04, 1, headersPayload) // HEADERS, END_HEADERS, stream 1
result, err := conn.ProcessFrames(frame, 0)
if err != nil {
t.Fatalf("ProcessFrames: %v", err)
}
if result == nil {
t.Fatal("result ne doit pas être nil")
}
// Check headers
headerMap := make(map[string]string)
for _, h := range result.Headers {
headerMap[h.Name] = h.Value
}
if headerMap[":method"] != "GET" {
t.Errorf(":method: attendu 'GET', obtenu %q", headerMap[":method"])
}
if headerMap[":path"] != "/" {
t.Errorf(":path: attendu '/', obtenu %q", headerMap[":path"])
}
if headerMap[":authority"] != "example.com" {
t.Errorf(":authority: attendu 'example.com', obtenu %q", headerMap[":authority"])
}
}
// TestH2ConnStateHeadersFullyIndexed verifies fully-indexed HPACK representations.
func TestH2ConnStateHeadersFullyIndexed(t *testing.T) {
conn := parser.NewH2ConnState()
// All fully-indexed: :method GET, :scheme https, :path /, accept */*
// Note: Go's hpack static table has index 19 as accept="" (no default value),
// unlike RFC 7541 which defines it as accept: */*. We test actual Go behavior.
headersPayload := []byte{
0x82, // :method GET
0x87, // :scheme https
0x84, // :path /
0x93, // accept (Go hpack: empty value; RFC 7541: */*)
}
frame := buildH2Frame(0x1, 0x04, 1, headersPayload)
result, err := conn.ProcessFrames(frame, 0)
if err != nil {
t.Fatalf("ProcessFrames: %v", err)
}
headerMap := make(map[string]string)
for _, h := range result.Headers {
headerMap[h.Name] = h.Value
}
if headerMap[":method"] != "GET" {
t.Errorf(":method: attendu 'GET', obtenu %q", headerMap[":method"])
}
if headerMap[":scheme"] != "https" {
t.Errorf(":scheme: attendu 'https', obtenu %q", headerMap[":scheme"])
}
if headerMap[":path"] != "/" {
t.Errorf(":path: attendu '/', obtenu %q", headerMap[":path"])
}
// Go's hpack emits accept="" for index 19 — verify it's present but empty
if _, ok := headerMap["accept"]; !ok {
t.Error("accept: header attendu mais absent")
}
}
// TestH2ConnStatePrefaceAndSettings verifies processing of H2 preface followed by SETTINGS.
func TestH2ConnStatePrefaceAndSettings(t *testing.T) {
// Client preface: magic + SETTINGS frame
preface := []byte("PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n")
// SETTINGS with INITIAL_WINDOW_SIZE=65536 and MAX_CONCURRENT_STREAMS=100
settingsPayload := []byte{
0x00, 0x04, 0x00, 0x00, 0xff, 0xff, // INITIAL_WINDOW_SIZE = 65535
0x00, 0x03, 0x00, 0x00, 0x00, 0x64, // MAX_CONCURRENT_STREAMS = 100
}
settingsFrame := buildH2Frame(0x4, 0x0, 0, settingsPayload)
data := append(preface, settingsFrame...)
// Detect preface and process remaining bytes
afterPreface := data[parser.H2MagicPrefaceLen():]
conn := parser.NewH2ConnState()
result, err := conn.ProcessFrames(afterPreface, 0)
if err != nil {
t.Fatalf("ProcessFrames: %v", err)
}
if result == nil || result.ClientSettings == nil {
t.Fatal("ClientSettings ne doit pas être nil")
}
if result.ClientSettings.InitialWindowSize != 65535 {
t.Errorf("InitialWindowSize: attendu 65535, obtenu %d", result.ClientSettings.InitialWindowSize)
}
if result.ClientSettings.MaxConcurrentStreams != 100 {
t.Errorf("MaxConcurrentStreams: attendu 100, obtenu %d", result.ClientSettings.MaxConcurrentStreams)
}
}
// TestH2ConnStateDynamicTable verifies that HPACK dynamic table works across multiple HEADERS frames.
func TestH2ConnStateDynamicTable(t *testing.T) {
conn := parser.NewH2ConnState()
// First HEADERS frame: :method GET, :authority example.com (literal with indexing)
// This adds "example.com" to the dynamic table
headers1 := []byte{
0x82, // :method GET (indexed)
0x41, // :authority with literal value (indexed in dynamic table)
0x0B, 'e', 'x', 'a', 'm', 'p', 'l', 'e', '.', 'c', 'o', 'm',
}
frame1 := buildH2Frame(0x1, 0x04, 1, headers1)
result1, _ := conn.ProcessFrames(frame1, 0)
if result1 == nil {
t.Fatal("result1 ne doit pas être nil")
}
headerMap1 := make(map[string]string)
for _, h := range result1.Headers {
headerMap1[h.Name] = h.Value
}
if headerMap1[":authority"] != "example.com" {
t.Errorf("first frame: :authority attendu 'example.com', obtenu %q", headerMap1[":authority"])
}
// Second HEADERS frame on stream 3: :method GET, :authority example.com (now in dynamic table)
// After adding "example.com" with index 62 in dynamic table, we can reference it
// However, for a simple test, we just verify the decoder still works
headers2 := []byte{
0x82, // :method GET (indexed)
0x84, // :path / (indexed)
}
frame2 := buildH2Frame(0x1, 0x04, 3, headers2)
result2, _ := conn.ProcessFrames(frame2, 0)
if result2 == nil {
t.Fatal("result2 ne doit pas être nil")
}
headerMap2 := make(map[string]string)
for _, h := range result2.Headers {
headerMap2[h.Name] = h.Value
}
if headerMap2[":method"] != "GET" {
t.Errorf("second frame: :method attendu 'GET', obtenu %q", headerMap2[":method"])
}
if headerMap2[":path"] != "/" {
t.Errorf("second frame: :path attendu '/', obtenu %q", headerMap2[":path"])
}
}
// TestH2ConnStateServerStatus verifies :status extraction from server HEADERS.
func TestH2ConnStateServerStatus(t *testing.T) {
conn := parser.NewH2ConnState()
// Server HEADERS frame with :status 200 (indexed, byte 0x88)
headersPayload := []byte{0x88} // :status 200
frame := buildH2Frame(0x1, 0x04, 1, headersPayload)
result, err := conn.ProcessFrames(frame, 1) // direction=1 (server→client)
if err != nil {
t.Fatalf("ProcessFrames: %v", err)
}
if result.StatusCode != 200 {
t.Errorf("StatusCode: attendu 200, obtenu %d", result.StatusCode)
}
}
// TestH2ConnStateGoAway verifies GOAWAY frame processing.
func TestH2ConnStateGoAway(t *testing.T) {
conn := parser.NewH2ConnState()
// GOAWAY frame: last stream ID = 0, error code = NO_ERROR (0)
goawayPayload := []byte{
0x00, 0x00, 0x00, 0x00, // last stream ID = 0
0x00, 0x00, 0x00, 0x00, // error code = NO_ERROR
}
frame := buildH2Frame(0x7, 0x0, 0, goawayPayload) // GOAWAY, stream 0
result, err := conn.ProcessFrames(frame, 1)
if err != nil {
t.Fatalf("ProcessFrames: %v", err)
}
if result.GoAwayLastStream != 0 {
t.Errorf("GoAwayLastStream: attendu 0, obtenu %d", result.GoAwayLastStream)
}
}
// TestIsH2FrameHeader verifies frame detection using http2.Framer.
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")
}
// Trop court
if parser.IsH2FrameHeader([]byte{0x00, 0x00}) {
t.Error("IsH2FrameHeader doit retourner false pour données trop courtes")
}
}
// TestH2ConnStateRSTStream verifies RST_STREAM frame processing.
func TestH2ConnStateRSTStream(t *testing.T) {
conn := parser.NewH2ConnState()
// RST_STREAM on stream 1 with error code CANCEL (0x08)
rstPayload := []byte{0x00, 0x00, 0x00, 0x08} // error code CANCEL
frame := buildH2Frame(0x3, 0x0, 1, rstPayload) // RST_STREAM, stream 1
result, _ := conn.ProcessFrames(frame, 1)
if result == nil {
t.Fatal("result ne doit pas être nil")
}
// Check that stream 1 is in the closed streams
found := false
for _, id := range result.StreamClosed {
if id == 1 {
found = true
}
}
if !found {
t.Error("stream 1 devrait être dans StreamClosed après RST_STREAM")
}
}
// TestHpackDecoderBasic verifies the hpack.Decoder works correctly via H2ConnState.
func TestHpackDecoderBasic(t *testing.T) {
// Create an H2ConnState and feed it a SETTINGS frame first (to set dynamic table size)
conn := parser.NewH2ConnState()
// SETTINGS with HEADER_TABLE_SIZE=4096
settingsPayload := []byte{
0x00, 0x01, 0x00, 0x00, 0x10, 0x00, // HEADER_TABLE_SIZE = 4096
}
settingsFrame := buildH2Frame(0x4, 0x0, 0, settingsPayload)
result, _ := conn.ProcessFrames(settingsFrame, 0)
if result.ClientSettings == nil || result.ClientSettings.HeaderTableSize != 4096 {
t.Errorf("HEADER_TABLE_SIZE: attendu 4096")
}
// Now feed a HEADERS frame with user-agent (literal with indexed name)
// user-agent is index 58 in HPACK static table
// 0x40 | 58 = 0x7A, then value length 8, then "curl/8.0"
uaPayload := []byte{
0x82, // :method GET
0x7A, // user-agent with literal value (indexed name 58)
0x08, 'c', 'u', 'r', 'l', '/', '8', '.', '0',
}
uaFrame := buildH2Frame(0x1, 0x04, 1, uaPayload)
result2, _ := conn.ProcessFrames(uaFrame, 0)
headerMap := make(map[string]string)
for _, h := range result2.Headers {
headerMap[h.Name] = h.Value
}
if headerMap["user-agent"] != "curl/8.0" {
t.Errorf("user-agent: attendu 'curl/8.0', obtenu %q", headerMap["user-agent"])
}
if headerMap[":method"] != "GET" {
t.Errorf(":method: attendu 'GET', obtenu %q", headerMap[":method"])
}
}
// TestH2ConnStateContinuation verifies HEADERS + CONTINUATION assembly.
func TestH2ConnStateContinuation(t *testing.T) {
conn := parser.NewH2ConnState()
// HEADERS frame WITHOUT END_HEADERS (flags=0x00, stream 1)
headersPayload := []byte{
0x82, // :method GET
0x84, // :path /
}
headersFrame := buildH2Frame(0x1, 0x00, 1, headersPayload) // HEADERS, NO END_HEADERS
// CONTINUATION frame WITH END_HEADERS (flags=0x04, stream 1)
contPayload := []byte{
0x41, // :authority with literal value
0x07, 'e', 'x', 'a', 'm', 'p', 'l', 'e',
}
contFrame := buildH2Frame(0x9, 0x04, 1, contPayload) // CONTINUATION, END_HEADERS
// Process both frames in one call
data := append(headersFrame, contFrame...)
result, _ := conn.ProcessFrames(data, 0)
if result == nil {
t.Fatal("result ne doit pas être nil")
}
headerMap := make(map[string]string)
for _, h := range result.Headers {
headerMap[h.Name] = h.Value
}
if headerMap[":method"] != "GET" {
t.Errorf(":method: attendu 'GET', obtenu %q", headerMap[":method"])
}
if headerMap[":path"] != "/" {
t.Errorf(":path: attendu '/', obtenu %q", headerMap[":path"])
}
if headerMap[":authority"] != "example" {
t.Errorf(":authority: attendu 'example', obtenu %q", headerMap[":authority"])
}
}
// TestH2ConnStatePing verifies PING frame counting.
func TestH2ConnStatePing(t *testing.T) {
conn := parser.NewH2ConnState()
// PING frame (8 bytes opaque data)
pingPayload := make([]byte, 8)
frame := buildH2Frame(0x6, 0x0, 0, pingPayload) // PING, stream 0
result, _ := conn.ProcessFrames(frame, 0)
if result == nil {
t.Fatal("result ne doit pas être nil")
}
count, ok := result.FrameCounts[http2.FramePing]
if !ok || count != 1 {
t.Errorf("PING frame count: attendu 1, obtenu %d", count)
}
}
// ---------------------------------------------------------------------------
// Phase 2 tests
// ---------------------------------------------------------------------------
// TestH2ConnStateSettingsAck verifies SETTINGS ACK detection.
func TestH2ConnStateSettingsAck(t *testing.T) {
conn := parser.NewH2ConnState()
// SETTINGS ACK frame (ACK flag = 0x01, no payload)
ackFrame := buildH2Frame(0x4, 0x01, 0, []byte{}) // SETTINGS, ACK flag
result, err := conn.ProcessFrames(ackFrame, 0)
if err != nil {
t.Fatalf("ProcessFrames: %v", err)
}
if !result.SettingsAckSeen {
t.Error("SettingsAckSeen devrait être true après SETTINGS ACK")
}
if !conn.SettingsAck {
t.Error("H2ConnState.SettingsAck devrait être true après SETTINGS ACK")
}
}
// TestH2ConnStatePingAck verifies PING ACK flag distinction.
func TestH2ConnStatePingAck(t *testing.T) {
conn := parser.NewH2ConnState()
// PING ACK frame (ACK flag = 0x01)
pingPayload := make([]byte, 8)
ackFrame := buildH2Frame(0x6, 0x01, 0, pingPayload) // PING, ACK flag
result, err := conn.ProcessFrames(ackFrame, 0)
if err != nil {
t.Fatalf("ProcessFrames: %v", err)
}
if !result.PingAckSeen {
t.Error("PingAckSeen devrait être true après PING ACK")
}
// Regular PING should NOT set PingAckSeen
conn2 := parser.NewH2ConnState()
regularPing := buildH2Frame(0x6, 0x0, 0, pingPayload) // PING, no ACK
result2, _ := conn2.ProcessFrames(regularPing, 0)
if result2.PingAckSeen {
t.Error("PingAckSeen ne devrait pas être true pour un PING régulier")
}
}
// TestH2ConnStatePriority verifies PRIORITY frame decoding.
func TestH2ConnStatePriority(t *testing.T) {
conn := parser.NewH2ConnState()
// Create stream 1 first (HEADERS)
headersPayload := []byte{0x82, 0x84} // :method GET, :path /
headersFrame := buildH2Frame(0x1, 0x04, 1, headersPayload)
conn.ProcessFrames(headersFrame, 0)
// PRIORITY frame on stream 1: StreamDep=0, Exclusive=false, Weight=15
// PRIORITY payload: 4 bytes (stream dep + exclusive bit) + 1 byte weight
priorityPayload := []byte{
0x00, 0x00, 0x00, 0x00, // StreamDep=0, Exclusive=false (bit 31 = 0)
0x0F, // Weight=15
}
priorityFrame := buildH2Frame(0x2, 0x0, 1, priorityPayload) // PRIORITY, stream 1
_, err := conn.ProcessFrames(priorityFrame, 0)
if err != nil {
t.Fatalf("ProcessFrames: %v", err)
}
stream, ok := conn.Streams[1]
if !ok {
t.Fatal("stream 1 devrait exister")
}
if stream.Priority == nil {
t.Fatal("stream.Priority ne devrait pas être nil après PRIORITY frame")
}
if stream.Priority.Weight != 15 {
t.Errorf("Weight: attendu 15, obtenu %d", stream.Priority.Weight)
}
if stream.Priority.StreamDep != 0 {
t.Errorf("StreamDep: attendu 0, obtenu %d", stream.Priority.StreamDep)
}
if stream.Priority.Exclusive {
t.Error("Exclusive devrait être false")
}
// Verify frame type history
found := false
for _, ft := range stream.FrameTypes {
if ft == http2.FramePriority {
found = true
}
}
if !found {
t.Error("PRIORITY devrait être dans FrameTypes du stream")
}
}
// TestH2ConnStatePerStreamWindowUpdate verifies per-stream WINDOW_UPDATE.
func TestH2ConnStatePerStreamWindowUpdate(t *testing.T) {
conn := parser.NewH2ConnState()
// Create stream 3 (client-initiated, odd)
headersPayload := []byte{0x82, 0x84} // :method GET, :path /
headersFrame := buildH2Frame(0x1, 0x04, 3, headersPayload)
conn.ProcessFrames(headersFrame, 0)
// WINDOW_UPDATE on stream 3 with increment = 32768
wuPayload := []byte{0x00, 0x00, 0x80, 0x00} // 32768
wuFrame := buildH2Frame(0x8, 0x0, 3, wuPayload) // WINDOW_UPDATE, stream 3
result, err := conn.ProcessFrames(wuFrame, 0)
if err != nil {
t.Fatalf("ProcessFrames: %v", err)
}
if result == nil {
t.Fatal("result ne doit pas être nil")
}
stream, ok := conn.Streams[3]
if !ok {
t.Fatal("stream 3 devrait exister")
}
if stream.WindowIncr != 32768 {
t.Errorf("WindowIncr: attendu 32768, obtenu %d", stream.WindowIncr)
}
}
// TestH2ConnStateFrameChronology verifies H2FrameRecord in results.
func TestH2ConnStateFrameChronology(t *testing.T) {
conn := parser.NewH2ConnState()
// SETTINGS frame
settingsPayload := []byte{
0x00, 0x01, 0x00, 0x00, 0x10, 0x00, // HEADER_TABLE_SIZE = 4096
}
settingsFrame := buildH2Frame(0x4, 0x0, 0, settingsPayload)
// HEADERS frame
headersPayload := []byte{0x82, 0x84} // :method GET, :path /
headersFrame := buildH2Frame(0x1, 0x04, 1, headersPayload)
// Process both frames in one call
data := append(settingsFrame, headersFrame...)
result, err := conn.ProcessFrames(data, 0)
if err != nil {
t.Fatalf("ProcessFrames: %v", err)
}
if len(result.Frames) != 2 {
t.Fatalf("Frames: attendu 2, obtenu %d", len(result.Frames))
}
// First frame: SETTINGS
f0 := result.Frames[0]
if f0.Index != 1 {
t.Errorf("Frame[0].Index: attendu 1, obtenu %d", f0.Index)
}
if f0.Direction != 0 {
t.Errorf("Frame[0].Direction: attendu 0, obtenu %d", f0.Direction)
}
if f0.Type != http2.FrameSettings {
t.Errorf("Frame[0].Type: attendu SETTINGS, obtenu %v", f0.Type)
}
if f0.StreamID != 0 {
t.Errorf("Frame[0].StreamID: attendu 0, obtenu %d", f0.StreamID)
}
// Second frame: HEADERS
f1 := result.Frames[1]
if f1.Index != 2 {
t.Errorf("Frame[1].Index: attendu 2, obtenu %d", f1.Index)
}
if f1.Type != http2.FrameHeaders {
t.Errorf("Frame[1].Type: attendu HEADERS, obtenu %v", f1.Type)
}
if f1.StreamID != 1 {
t.Errorf("Frame[1].StreamID: attendu 1, obtenu %d", f1.StreamID)
}
}
// TestH2ConnStateStreamInitiator verifies stream initiator tracking.
func TestH2ConnStateStreamInitiator(t *testing.T) {
conn := parser.NewH2ConnState()
// Stream 1 (client, odd)
h1 := []byte{0x82, 0x84} // :method GET, :path /
frame1 := buildH2Frame(0x1, 0x04, 1, h1)
conn.ProcessFrames(frame1, 0)
// Stream 2 (server, even) — server-initiated push promise
h2 := []byte{0x88} // :status 200
frame2 := buildH2Frame(0x1, 0x04, 2, h2)
conn.ProcessFrames(frame2, 1)
stream1, ok1 := conn.Streams[1]
if !ok1 {
t.Fatal("stream 1 devrait exister")
}
if stream1.Initiator != 0 {
t.Errorf("stream 1 Initiator: attendu 0 (client), obtenu %d", stream1.Initiator)
}
stream2, ok2 := conn.Streams[2]
if !ok2 {
t.Fatal("stream 2 devrait exister")
}
if stream2.Initiator != 1 {
t.Errorf("stream 2 Initiator: attendu 1 (serveur), obtenu %d", stream2.Initiator)
}
}
// TestH2ConnStateStreamStateMachine verifies open → half-closed → closed transitions.
func TestH2ConnStateStreamStateMachine(t *testing.T) {
conn := parser.NewH2ConnState()
// Stream 1: HEADERS with END_STREAM (client sends request + END_STREAM)
h1 := []byte{0x82, 0x84} // :method GET, :path /
frame1 := buildH2Frame(0x1, 0x05, 1, h1) // HEADERS, END_STREAM + END_HEADERS
conn.ProcessFrames(frame1, 0)
stream1, ok := conn.Streams[1]
if !ok {
t.Fatal("stream 1 devrait exister")
}
if stream1.State != "half-closed-remote" {
t.Errorf("après END_STREAM client: état attendu 'half-closed-remote', obtenu %q", stream1.State)
}
// Server responds with END_STREAM → closed
h2 := []byte{0x88} // :status 200
frame2 := buildH2Frame(0x1, 0x05, 1, h2) // HEADERS, END_STREAM + END_HEADERS
conn.ProcessFrames(frame2, 1)
if stream1.State != "closed" {
t.Errorf("après END_STREAM serveur: état attendu 'closed', obtenu %q", stream1.State)
}
}
// TestH2ConnStateStreamFrameHistory verifies FrameTypes accumulation per stream.
func TestH2ConnStateStreamFrameHistory(t *testing.T) {
conn := parser.NewH2ConnState()
// HEADERS on stream 1
h1 := []byte{0x82, 0x84}
frame1 := buildH2Frame(0x1, 0x04, 1, h1) // HEADERS, END_HEADERS
conn.ProcessFrames(frame1, 0)
// DATA on stream 1
dataPayload := []byte("hello")
dataFrame := buildH2Frame(0x0, 0x01, 1, dataPayload) // DATA, END_STREAM
conn.ProcessFrames(dataFrame, 0)
stream, ok := conn.Streams[1]
if !ok {
t.Fatal("stream 1 devrait exister")
}
if len(stream.FrameTypes) != 2 {
t.Fatalf("FrameTypes: attendu 2, obtenu %d", len(stream.FrameTypes))
}
if stream.FrameTypes[0] != http2.FrameHeaders {
t.Errorf("FrameTypes[0]: attendu HEADERS, obtenu %v", stream.FrameTypes[0])
}
if stream.FrameTypes[1] != http2.FrameData {
t.Errorf("FrameTypes[1]: attendu DATA, obtenu %v", stream.FrameTypes[1])
}
}
// TestH2ConnStateMultipleFramesInBatch verifies frame index persistence across calls.
func TestH2ConnStateMultipleFramesInBatch(t *testing.T) {
conn := parser.NewH2ConnState()
// First call: SETTINGS + HEADERS
settingsPayload := []byte{
0x00, 0x01, 0x00, 0x00, 0x10, 0x00, // HEADER_TABLE_SIZE = 4096
}
settingsFrame := buildH2Frame(0x4, 0x0, 0, settingsPayload)
h1 := []byte{0x82, 0x84}
headersFrame := buildH2Frame(0x1, 0x04, 1, h1)
data1 := append(settingsFrame, headersFrame...)
result1, _ := conn.ProcessFrames(data1, 0)
if len(result1.Frames) != 2 {
t.Fatalf("Batch 1: attendu 2 frames, obtenu %d", len(result1.Frames))
}
if result1.Frames[0].Index != 1 || result1.Frames[1].Index != 2 {
t.Errorf("Batch 1 indices: attendu [1,2], obtenu [%d,%d]", result1.Frames[0].Index, result1.Frames[1].Index)
}
// Second call: PING → index should continue at 3
pingPayload := make([]byte, 8)
pingFrame := buildH2Frame(0x6, 0x0, 0, pingPayload)
result2, _ := conn.ProcessFrames(pingFrame, 0)
if len(result2.Frames) != 1 {
t.Fatalf("Batch 2: attendu 1 frame, obtenu %d", len(result2.Frames))
}
if result2.Frames[0].Index != 3 {
t.Errorf("Batch 2 index: attendu 3, obtenu %d", result2.Frames[0].Index)
}
}