// 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, ",") }