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>
This commit is contained in:
646
services/ja4ebpf/internal/parser/h2conn.go
Normal file
646
services/ja4ebpf/internal/parser/h2conn.go
Normal file
@ -0,0 +1,646 @@
|
||||
// Package parser fournit le parseur HTTP/2 basé sur golang.org/x/net/http2
|
||||
// et le décodeur HPACK pour l'extraction des empreintes de fingerprinting réseau.
|
||||
package parser
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/net/http2"
|
||||
"golang.org/x/net/http2/hpack"
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Constantes HTTP/2 (RFC 9113)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// H2Magic est la préface HTTP/2 client (RFC 7540 §3.5), exportée pour usage
|
||||
// par le routeur Magic Bytes (package dispatcher) et les consommateurs RingBuffer.
|
||||
const H2Magic = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"
|
||||
|
||||
// h2MagicPrefaceLen est la longueur du préambule HTTP/2 client.
|
||||
const h2MagicPrefaceLen = 24
|
||||
|
||||
// h2MagicPreface est le préambule ("PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n") envoyé
|
||||
// par tout client HTTP/2 avant la première frame SETTINGS.
|
||||
var h2MagicPreface = []byte(H2Magic)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types exportés
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// H2FrameRecord est un enregistrement par frame dans le résultat de ProcessFrames.
|
||||
// Fournit une chronologie fine des frames HTTP/2 avec offset logique, direction et métadonnées.
|
||||
type H2FrameRecord struct {
|
||||
Index uint32 // offset logique (incrémenté par frame dans H2ConnState)
|
||||
Direction uint8 // 0=client→serveur, 1=serveur→client
|
||||
Type http2.FrameType // type de frame (DATA, HEADERS, SETTINGS, etc.)
|
||||
Flags http2.Flags // drapeaux de la frame
|
||||
StreamID uint32 // ID du stream (0 pour les frames de connexion)
|
||||
Length uint32 // longueur du payload en octets
|
||||
}
|
||||
|
||||
// H2Priority contient les paramètres de priorité d'un stream HTTP/2 (RFC 9113 §5.3).
|
||||
type H2Priority struct {
|
||||
StreamDep uint32 // stream dépendant
|
||||
Exclusive bool // priorité exclusive
|
||||
Weight uint8 // poids (1-256)
|
||||
}
|
||||
|
||||
// HTTP2Settings contient les paramètres SETTINGS et WINDOW_UPDATE du client HTTP/2.
|
||||
type HTTP2Settings struct {
|
||||
HeaderTableSize int32 // SETTINGS_HEADER_TABLE_SIZE (-1 si absent)
|
||||
EnablePush int32 // SETTINGS_ENABLE_PUSH
|
||||
MaxConcurrentStreams int32 // SETTINGS_MAX_CONCURRENT_STREAMS
|
||||
InitialWindowSize int32 // SETTINGS_INITIAL_WINDOW_SIZE
|
||||
MaxFrameSize int32 // SETTINGS_MAX_FRAME_SIZE
|
||||
MaxHeaderListSize int32 // SETTINGS_MAX_HEADER_LIST_SIZE
|
||||
UnknownSettings int32 // paramètre 0x7 (JA4H2)
|
||||
EnableConnectProtocol int32 // SETTINGS_ENABLE_CONNECT_PROTOCOL (0x8, RFC 8441)
|
||||
WindowUpdateIncrement uint32 // valeur WINDOW_UPDATE sur stream 0
|
||||
PseudoHeaderOrder []string
|
||||
HeaderKV map[string]string // en-têtes extraits
|
||||
HeaderOrder []string // noms des en-têtes dans l'ordre d'arrivée
|
||||
}
|
||||
|
||||
// CapturedHeader est un en-tête HTTP/2 capturé avec son nom et sa valeur.
|
||||
type CapturedHeader struct {
|
||||
Name string
|
||||
Value string
|
||||
}
|
||||
|
||||
// H2FrameResult contient les données extraites d'un appel à ProcessFrames.
|
||||
type H2FrameResult struct {
|
||||
// En-têtes décodés (HEADERS + CONTINUATION assemblés)
|
||||
Headers []CapturedHeader
|
||||
HeaderStreamID uint32
|
||||
|
||||
// Paramètres SETTINGS
|
||||
ClientSettings *HTTP2Settings // non-nil si frame SETTINGS client vue
|
||||
ServerSettings *HTTP2Settings // non-nil si frame SETTINGS serveur vue
|
||||
|
||||
// WINDOW_UPDATE sur stream 0 (connexion)
|
||||
ConnWindowUpdate uint32
|
||||
|
||||
// Code de statut HTTP (:status extrait des en-têtes serveur)
|
||||
StatusCode int
|
||||
|
||||
// Streams fermés (END_STREAM ou RST_STREAM)
|
||||
StreamClosed []uint32
|
||||
|
||||
// GOAWAY
|
||||
GoAwayLastStream uint32
|
||||
GoAwayErrCode http2.ErrCode
|
||||
|
||||
// Compteurs de frames par type
|
||||
FrameCounts map[http2.FrameType]int
|
||||
|
||||
// Préface détectée
|
||||
PrefaceDetected bool
|
||||
|
||||
// Pseudo-headers extraits (ordre)
|
||||
PseudoHeaderOrder []string
|
||||
|
||||
// NOUVEAU Phase 2 : chronologie des frames de cet appel
|
||||
Frames []H2FrameRecord
|
||||
|
||||
// NOUVEAU Phase 2 : compteurs fine-grained
|
||||
SettingsAckSeen bool // SETTINGS ACK reçu dans ce batch
|
||||
PingAckSeen bool // PING ACK reçu dans ce batch
|
||||
}
|
||||
|
||||
// H2StreamState suit l'état d'un stream HTTP/2.
|
||||
type H2StreamState struct {
|
||||
ID uint32
|
||||
Initiator uint8 // 0=client (impair), 1=serveur (pair)
|
||||
State string // "idle", "open", "half-closed-local", "half-closed-remote", "closed"
|
||||
EndStream bool
|
||||
DataBytes int64
|
||||
RSTCode uint32
|
||||
Priority *H2Priority // non-nil si frame PRIORITY reçue
|
||||
WindowIncr uint32 // WINDOW_UPDATE incrément cumulé sur ce stream
|
||||
FrameTypes []http2.FrameType // historique condensé (types uniquement, pas payload)
|
||||
}
|
||||
|
||||
// H2ConnState maintient l'état par-connexion HTTP/2, incluant le décodeur HPACK.
|
||||
// Stocké dans correlation.SessionState et persisté entre les événements SSL_read.
|
||||
type H2ConnState struct {
|
||||
hdec *hpack.Decoder // décodeur HPACK par-connexion (table dynamique)
|
||||
headerBuf bytes.Buffer // fragments HEADERS+CONTINUATION en attente
|
||||
headerFragStream uint32 // stream ID des fragments en attente
|
||||
|
||||
// État de connexion
|
||||
ClientSettings *HTTP2Settings
|
||||
ServerSettings *HTTP2Settings
|
||||
FrameCounts map[http2.FrameType]int
|
||||
PrefaceSeen bool
|
||||
|
||||
// Suivi des streams
|
||||
Streams map[uint32]*H2StreamState
|
||||
|
||||
// GOAWAY
|
||||
LastStreamID uint32
|
||||
GoAwayErr http2.ErrCode
|
||||
|
||||
// NOUVEAU Phase 2
|
||||
frameIndex uint32 // compteur logique de frames (persisté entre appels)
|
||||
SettingsAck bool // SETTINGS ACK reçu (client→serveur)
|
||||
ServerAck bool // SETTINGS ACK reçu (serveur→client)
|
||||
}
|
||||
|
||||
// NewH2ConnState crée un nouvel état de connexion HTTP/2 avec un décodeur HPACK frais.
|
||||
func NewH2ConnState() *H2ConnState {
|
||||
return &H2ConnState{
|
||||
hdec: hpack.NewDecoder(4096, nil),
|
||||
FrameCounts: make(map[http2.FrameType]int),
|
||||
Streams: make(map[uint32]*H2StreamState),
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Détection du préambule HTTP/2 (fonctions utilitaires exportées)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// DetectH2Preface vérifie si le buffer commence par le préambule HTTP/2.
|
||||
func DetectH2Preface(data []byte) bool {
|
||||
if len(data) < h2MagicPrefaceLen {
|
||||
return false
|
||||
}
|
||||
for i := 0; i < h2MagicPrefaceLen; i++ {
|
||||
if data[i] != h2MagicPreface[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// H2MagicPrefaceLen retourne la longueur du préambule HTTP/2.
|
||||
func H2MagicPrefaceLen() int {
|
||||
return h2MagicPrefaceLen
|
||||
}
|
||||
|
||||
// IsH2FrameHeader vérifie si les données commencent par un en-tête de frame HTTP/2 valide.
|
||||
// Utilisé comme détection rapide avant de créer un H2ConnState.
|
||||
func IsH2FrameHeader(data []byte) bool {
|
||||
if len(data) < 9 {
|
||||
return false
|
||||
}
|
||||
r := bytes.NewReader(data)
|
||||
fr := http2.NewFramer(io.Discard, r)
|
||||
fr.AllowIllegalReads = true
|
||||
_, err := fr.ReadFrame()
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Traitement des frames HTTP/2
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// ProcessFrames parse les frames HTTP/2 depuis sslData via http2.Framer,
|
||||
// met à jour l'état de connexion, et retourne les en-têtes décodés.
|
||||
// direction: 0 = client→serveur, 1 = serveur→client.
|
||||
func (c *H2ConnState) ProcessFrames(data []byte, direction uint8) (*H2FrameResult, error) {
|
||||
r := bytes.NewReader(data)
|
||||
fr := http2.NewFramer(io.Discard, r)
|
||||
fr.AllowIllegalReads = true
|
||||
// NE PAS positionner ReadMetaHeaders — on gère HPACK nous-mêmes
|
||||
// pour maintenir la table dynamique par-connexion.
|
||||
|
||||
result := &H2FrameResult{
|
||||
FrameCounts: make(map[http2.FrameType]int),
|
||||
}
|
||||
|
||||
for {
|
||||
frame, err := fr.ReadFrame()
|
||||
if err != nil {
|
||||
// io.EOF ou données insuffisantes → fin du traitement
|
||||
break
|
||||
}
|
||||
|
||||
c.frameIndex++
|
||||
|
||||
// Enregistrer la frame dans le résultat
|
||||
rec := H2FrameRecord{
|
||||
Index: c.frameIndex,
|
||||
Direction: direction,
|
||||
Type: frame.Header().Type,
|
||||
Flags: http2.Flags(frame.Header().Flags),
|
||||
StreamID: frame.Header().StreamID,
|
||||
Length: frame.Header().Length,
|
||||
}
|
||||
result.Frames = append(result.Frames, rec)
|
||||
|
||||
c.FrameCounts[frame.Header().Type]++
|
||||
result.FrameCounts[frame.Header().Type]++
|
||||
|
||||
switch f := frame.(type) {
|
||||
case *http2.SettingsFrame:
|
||||
if f.IsAck() {
|
||||
if direction == 0 {
|
||||
c.SettingsAck = true
|
||||
result.SettingsAckSeen = true
|
||||
} else {
|
||||
c.ServerAck = true
|
||||
}
|
||||
} else {
|
||||
c.processSettings(f, direction, result)
|
||||
}
|
||||
case *http2.HeadersFrame:
|
||||
c.processHeaders(f, direction, result)
|
||||
case *http2.ContinuationFrame:
|
||||
c.processContinuation(f, result)
|
||||
case *http2.WindowUpdateFrame:
|
||||
c.processWindowUpdate(f, direction, result)
|
||||
case *http2.DataFrame:
|
||||
c.processData(f, direction, result)
|
||||
case *http2.PingFrame:
|
||||
if f.IsAck() {
|
||||
result.PingAckSeen = true
|
||||
}
|
||||
case *http2.GoAwayFrame:
|
||||
c.processGoAway(f, result)
|
||||
case *http2.RSTStreamFrame:
|
||||
c.processRSTStream(f, result)
|
||||
case *http2.PriorityFrame:
|
||||
c.processPriority(f, result)
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Traitement des frames individuelles
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func (c *H2ConnState) processSettings(f *http2.SettingsFrame, direction uint8, result *H2FrameResult) {
|
||||
settings := &HTTP2Settings{
|
||||
HeaderTableSize: -1,
|
||||
EnablePush: -1,
|
||||
MaxConcurrentStreams: -1,
|
||||
InitialWindowSize: -1,
|
||||
MaxFrameSize: -1,
|
||||
MaxHeaderListSize: -1,
|
||||
EnableConnectProtocol: -1,
|
||||
}
|
||||
|
||||
f.ForeachSetting(func(s http2.Setting) error {
|
||||
switch s.ID {
|
||||
case http2.SettingHeaderTableSize:
|
||||
settings.HeaderTableSize = int32(s.Val)
|
||||
// Mettre à jour la taille de la table dynamique HPACK côté client
|
||||
if direction == 0 {
|
||||
c.hdec.SetMaxDynamicTableSize(s.Val)
|
||||
}
|
||||
case http2.SettingEnablePush:
|
||||
settings.EnablePush = int32(s.Val)
|
||||
case http2.SettingMaxConcurrentStreams:
|
||||
settings.MaxConcurrentStreams = int32(s.Val)
|
||||
case http2.SettingInitialWindowSize:
|
||||
settings.InitialWindowSize = int32(s.Val)
|
||||
case http2.SettingMaxFrameSize:
|
||||
settings.MaxFrameSize = int32(s.Val)
|
||||
case http2.SettingMaxHeaderListSize:
|
||||
settings.MaxHeaderListSize = int32(s.Val)
|
||||
case http2.SettingEnableConnectProtocol:
|
||||
settings.EnableConnectProtocol = int32(s.Val)
|
||||
case 7: // paramètre non standard (JA4H2)
|
||||
settings.UnknownSettings = int32(s.Val)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if direction == 0 {
|
||||
// Client SETTINGS → merger avec l'existant
|
||||
c.ClientSettings = mergeSettings(c.ClientSettings, settings)
|
||||
result.ClientSettings = c.ClientSettings
|
||||
} else {
|
||||
c.ServerSettings = mergeSettings(c.ServerSettings, settings)
|
||||
result.ServerSettings = c.ServerSettings
|
||||
}
|
||||
}
|
||||
|
||||
// mergeSettings fusionne les nouveaux paramètres dans les existants.
|
||||
// Les paramètres non-présents dans le nouveau gardent leur valeur existante.
|
||||
func mergeSettings(base, new *HTTP2Settings) *HTTP2Settings {
|
||||
if base == nil {
|
||||
return new
|
||||
}
|
||||
if new == nil {
|
||||
return base
|
||||
}
|
||||
// Le nouveau remplace les champs présents (valeur >= 0)
|
||||
if new.HeaderTableSize >= 0 {
|
||||
base.HeaderTableSize = new.HeaderTableSize
|
||||
}
|
||||
if new.EnablePush >= 0 {
|
||||
base.EnablePush = new.EnablePush
|
||||
}
|
||||
if new.MaxConcurrentStreams >= 0 {
|
||||
base.MaxConcurrentStreams = new.MaxConcurrentStreams
|
||||
}
|
||||
if new.InitialWindowSize >= 0 {
|
||||
base.InitialWindowSize = new.InitialWindowSize
|
||||
}
|
||||
if new.MaxFrameSize >= 0 {
|
||||
base.MaxFrameSize = new.MaxFrameSize
|
||||
}
|
||||
if new.MaxHeaderListSize >= 0 {
|
||||
base.MaxHeaderListSize = new.MaxHeaderListSize
|
||||
}
|
||||
if new.EnableConnectProtocol >= 0 {
|
||||
base.EnableConnectProtocol = new.EnableConnectProtocol
|
||||
}
|
||||
if new.UnknownSettings >= 0 {
|
||||
base.UnknownSettings = new.UnknownSettings
|
||||
}
|
||||
// Conserver les données d'en-têtes existantes si le nouveau n'en a pas
|
||||
if len(new.PseudoHeaderOrder) > 0 {
|
||||
base.PseudoHeaderOrder = new.PseudoHeaderOrder
|
||||
}
|
||||
if len(new.HeaderKV) > 0 {
|
||||
base.HeaderKV = new.HeaderKV
|
||||
}
|
||||
if len(new.HeaderOrder) > 0 {
|
||||
base.HeaderOrder = new.HeaderOrder
|
||||
}
|
||||
return base
|
||||
}
|
||||
|
||||
func (c *H2ConnState) processHeaders(f *http2.HeadersFrame, direction uint8, result *H2FrameResult) {
|
||||
streamID := f.StreamID
|
||||
|
||||
// Créer le stream si nécessaire
|
||||
if streamID > 0 {
|
||||
if _, ok := c.Streams[streamID]; !ok {
|
||||
initiator := uint8(0) // client (impair)
|
||||
if streamID%2 == 0 {
|
||||
initiator = 1 // serveur (pair)
|
||||
}
|
||||
c.Streams[streamID] = &H2StreamState{ID: streamID, Initiator: initiator, State: "open"}
|
||||
}
|
||||
stream := c.Streams[streamID]
|
||||
stream.FrameTypes = append(stream.FrameTypes, http2.FrameHeaders)
|
||||
}
|
||||
|
||||
// Buffer le fragment de bloc d'en-têtes
|
||||
c.headerBuf.Write(f.HeaderBlockFragment())
|
||||
c.headerFragStream = streamID
|
||||
|
||||
// END_STREAM sur la frame HEADERS → transition d'état
|
||||
if f.StreamEnded() {
|
||||
c.transitionStream(streamID, direction, result)
|
||||
}
|
||||
|
||||
// Si END_HEADERS est positionné, décoder immédiatement
|
||||
if f.Flags&http2.FlagHeadersEndHeaders != 0 {
|
||||
c.decodeHeaders(result)
|
||||
}
|
||||
// Sinon, attendre les frames CONTINUATION
|
||||
}
|
||||
|
||||
func (c *H2ConnState) processContinuation(f *http2.ContinuationFrame, result *H2FrameResult) {
|
||||
c.headerBuf.Write(f.HeaderBlockFragment())
|
||||
|
||||
// Si END_HEADERS est positionné, décoder le bloc complet
|
||||
if f.Flags&http2.FlagContinuationEndHeaders != 0 {
|
||||
c.decodeHeaders(result)
|
||||
}
|
||||
}
|
||||
|
||||
// decodeHeaders décode le bloc HPACK accumulé en utilisant le décodeur par-connexion.
|
||||
// La table dynamique HPACK est maintenue entre les appels.
|
||||
func (c *H2ConnState) decodeHeaders(result *H2FrameResult) {
|
||||
var headers []CapturedHeader
|
||||
|
||||
c.hdec.SetEmitFunc(func(hf hpack.HeaderField) {
|
||||
headers = append(headers, CapturedHeader{
|
||||
Name: hf.Name,
|
||||
Value: hf.Value,
|
||||
})
|
||||
})
|
||||
|
||||
c.hdec.Write(c.headerBuf.Bytes())
|
||||
c.hdec.Close() // Finalise le bloc HPACK, préserve la table dynamique
|
||||
|
||||
streamID := c.headerFragStream
|
||||
c.headerBuf.Reset()
|
||||
c.headerFragStream = 0
|
||||
|
||||
result.Headers = headers
|
||||
result.HeaderStreamID = streamID
|
||||
|
||||
// Extraire les pseudo-headers et en-têtes capturés
|
||||
var pseudoOrder []string
|
||||
kv := make(map[string]string)
|
||||
var order []string
|
||||
|
||||
for _, h := range headers {
|
||||
nameLower := strings.ToLower(h.Name)
|
||||
if strings.HasPrefix(nameLower, ":") {
|
||||
pseudoOrder = append(pseudoOrder, nameLower)
|
||||
}
|
||||
if HpackCapturedHeaders[nameLower] && h.Value != "" {
|
||||
kv[nameLower] = h.Value
|
||||
order = append(order, nameLower)
|
||||
}
|
||||
}
|
||||
|
||||
result.PseudoHeaderOrder = pseudoOrder
|
||||
|
||||
// Extraire :status de tout stream (serveur ou client)
|
||||
for _, h := range headers {
|
||||
if strings.ToLower(h.Name) == ":status" {
|
||||
if code, err := strconv.Atoi(h.Value); err == nil && code >= 100 && code <= 599 {
|
||||
result.StatusCode = code
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Si on a des en-têtes, mettre à jour les ClientSettings
|
||||
// avec les données d'en-têtes (pour la session HTTP/2)
|
||||
if streamID > 0 && len(kv) > 0 {
|
||||
isClientStream := streamID%2 == 1
|
||||
|
||||
if isClientStream {
|
||||
// Mettre à jour les en-têtes du client
|
||||
if c.ClientSettings != nil {
|
||||
if len(pseudoOrder) > 0 {
|
||||
c.ClientSettings.PseudoHeaderOrder = pseudoOrder
|
||||
}
|
||||
if c.ClientSettings.HeaderKV == nil {
|
||||
c.ClientSettings.HeaderKV = make(map[string]string)
|
||||
}
|
||||
for k, v := range kv {
|
||||
c.ClientSettings.HeaderKV[k] = v
|
||||
}
|
||||
if len(order) > 0 {
|
||||
c.ClientSettings.HeaderOrder = order
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *H2ConnState) processWindowUpdate(f *http2.WindowUpdateFrame, direction uint8, result *H2FrameResult) {
|
||||
if f.StreamID == 0 {
|
||||
// WINDOW_UPDATE sur le flux de connexion
|
||||
if direction == 0 {
|
||||
// Client
|
||||
if c.ClientSettings != nil {
|
||||
c.ClientSettings.WindowUpdateIncrement = f.Increment
|
||||
}
|
||||
}
|
||||
result.ConnWindowUpdate = f.Increment
|
||||
} else {
|
||||
// WINDOW_UPDATE per-stream
|
||||
if stream, ok := c.Streams[f.StreamID]; ok {
|
||||
stream.WindowIncr += f.Increment
|
||||
stream.FrameTypes = append(stream.FrameTypes, http2.FrameWindowUpdate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *H2ConnState) processData(f *http2.DataFrame, direction uint8, result *H2FrameResult) {
|
||||
streamID := f.Header().StreamID
|
||||
if stream, ok := c.Streams[streamID]; ok {
|
||||
stream.DataBytes += int64(len(f.Data()))
|
||||
stream.FrameTypes = append(stream.FrameTypes, http2.FrameData)
|
||||
}
|
||||
if f.StreamEnded() {
|
||||
c.transitionStream(streamID, direction, result)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *H2ConnState) processGoAway(f *http2.GoAwayFrame, result *H2FrameResult) {
|
||||
c.LastStreamID = f.LastStreamID
|
||||
c.GoAwayErr = f.ErrCode
|
||||
result.GoAwayLastStream = f.LastStreamID
|
||||
result.GoAwayErrCode = f.ErrCode
|
||||
}
|
||||
|
||||
func (c *H2ConnState) processRSTStream(f *http2.RSTStreamFrame, result *H2FrameResult) {
|
||||
streamID := f.Header().StreamID
|
||||
if stream, ok := c.Streams[streamID]; ok {
|
||||
stream.RSTCode = uint32(f.ErrCode)
|
||||
stream.State = "closed"
|
||||
stream.FrameTypes = append(stream.FrameTypes, http2.FrameRSTStream)
|
||||
}
|
||||
result.StreamClosed = append(result.StreamClosed, streamID)
|
||||
}
|
||||
|
||||
// processPriority traite les frames PRIORITY (RFC 9113 §5.3).
|
||||
func (c *H2ConnState) processPriority(f *http2.PriorityFrame, result *H2FrameResult) {
|
||||
streamID := f.Header().StreamID
|
||||
if stream, ok := c.Streams[streamID]; ok {
|
||||
stream.Priority = &H2Priority{
|
||||
StreamDep: f.PriorityParam.StreamDep,
|
||||
Exclusive: f.PriorityParam.Exclusive,
|
||||
Weight: f.PriorityParam.Weight,
|
||||
}
|
||||
stream.FrameTypes = append(stream.FrameTypes, http2.FramePriority)
|
||||
}
|
||||
}
|
||||
|
||||
// transitionStream gère les transitions d'état du stream HTTP/2 (RFC 9113 §5.1).
|
||||
func (c *H2ConnState) transitionStream(streamID uint32, direction uint8, result *H2FrameResult) {
|
||||
stream, ok := c.Streams[streamID]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if stream.State == "closed" {
|
||||
return
|
||||
}
|
||||
|
||||
switch {
|
||||
case stream.State == "open" && direction == 0:
|
||||
stream.State = "half-closed-remote"
|
||||
case stream.State == "open" && direction == 1:
|
||||
stream.State = "half-closed-local"
|
||||
case stream.State == "half-closed-local" || stream.State == "half-closed-remote":
|
||||
stream.State = "closed"
|
||||
default:
|
||||
stream.State = "closed"
|
||||
}
|
||||
|
||||
stream.EndStream = true
|
||||
result.StreamClosed = append(result.StreamClosed, streamID)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// En-têtes capturés — réutilise hpackCapturedHeaders de http2.go
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Utilitaires de formatage
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// PseudoOrderToShort convertit la liste de pseudo-headers en notation abrégée.
|
||||
// Ex: [":method", ":authority", ":scheme", ":path"] → "m,a,s,p"
|
||||
func PseudoOrderToShort(headers []string) string {
|
||||
short := make([]byte, 0, len(headers)*2-1)
|
||||
for i, h := range headers {
|
||||
if i > 0 {
|
||||
short = append(short, ',')
|
||||
}
|
||||
switch {
|
||||
case h == ":method":
|
||||
short = append(short, 'm')
|
||||
case h == ":authority":
|
||||
short = append(short, 'a')
|
||||
case h == ":scheme":
|
||||
short = append(short, 's')
|
||||
case h == ":path":
|
||||
short = append(short, 'p')
|
||||
case h == ":status":
|
||||
short = append(short, 't')
|
||||
default:
|
||||
short = append(short, '?')
|
||||
}
|
||||
}
|
||||
return string(short)
|
||||
}
|
||||
|
||||
// FrameTypeString retourne le nom lisible d'un type de frame HTTP/2.
|
||||
func FrameTypeString(t http2.FrameType) string {
|
||||
switch t {
|
||||
case http2.FrameData:
|
||||
return "DATA"
|
||||
case http2.FrameHeaders:
|
||||
return "HEADERS"
|
||||
case http2.FramePriority:
|
||||
return "PRIORITY"
|
||||
case http2.FrameRSTStream:
|
||||
return "RST_STREAM"
|
||||
case http2.FrameSettings:
|
||||
return "SETTINGS"
|
||||
case http2.FramePushPromise:
|
||||
return "PUSH_PROMISE"
|
||||
case http2.FramePing:
|
||||
return "PING"
|
||||
case http2.FrameGoAway:
|
||||
return "GOAWAY"
|
||||
case http2.FrameWindowUpdate:
|
||||
return "WINDOW_UPDATE"
|
||||
case http2.FrameContinuation:
|
||||
return "CONTINUATION"
|
||||
default:
|
||||
return fmt.Sprintf("UNKNOWN(%d)", t)
|
||||
}
|
||||
}
|
||||
|
||||
// FrameCountsToString sérialise les compteurs de frames en chaîne lisible.
|
||||
func FrameCountsToString(counts map[http2.FrameType]int) string {
|
||||
if len(counts) == 0 {
|
||||
return ""
|
||||
}
|
||||
parts := make([]string, 0, len(counts))
|
||||
for t, n := range counts {
|
||||
parts = append(parts, fmt.Sprintf("%s:%d", FrameTypeString(t), n))
|
||||
}
|
||||
return strings.Join(parts, ",")
|
||||
}
|
||||
Reference in New Issue
Block a user