- 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>
735 lines
23 KiB
Go
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)
|
|
}
|
|
} |