package parser import ( "encoding/binary" "fmt" "strings" ) // 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) // Identifiants de types de frames HTTP/2 (RFC 7540, §11.2). const ( h2FrameData = 0 h2FrameHeaders = 1 h2FramePriority = 2 h2FrameRSTStream = 3 h2FrameSettings = 4 h2FramePushPromise = 5 h2FramePing = 6 h2FrameGoAway = 7 h2FrameWindowUpdate = 8 h2FrameContinuation = 9 ) // Identifiants des paramètres SETTINGS (RFC 7540, §11.3 + RFC 8441). const ( h2SettingHeaderTableSize = 1 h2SettingEnablePush = 2 h2SettingMaxConcurrentStreams = 3 h2SettingInitialWindowSize = 4 h2SettingMaxFrameSize = 5 h2SettingMaxHeaderListSize = 6 h2SettingEnableConnectProtocol = 8 ) // h2FrameHeader représente l'en-tête fixe de 9 octets d'une frame HTTP/2. type h2FrameHeader struct { Length uint32 // longueur du payload (3 octets) Type uint8 // type de frame Flags uint8 // flags StreamID uint32 // identifiant de stream (masque 0x7FFFFFFF) } // parseH2FrameHeader décode l'en-tête de 9 octets d'une frame HTTP/2. func parseH2FrameHeader(data []byte) (h2FrameHeader, error) { if len(data) < 9 { return h2FrameHeader{}, fmt.Errorf("données insuffisantes pour l'en-tête frame HTTP/2: %d octets", len(data)) } // Longueur sur 3 octets big-endian length := uint32(data[0])<<16 | uint32(data[1])<<8 | uint32(data[2]) return h2FrameHeader{ Length: length, Type: data[3], Flags: data[4], StreamID: binary.BigEndian.Uint32(data[5:9]) & 0x7FFFFFFF, }, nil } // 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 } // 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 // ordre des pseudo-headers [:method, :authority, ...] HeaderKV map[string]string // en-têtes extraits du premier HEADERS frame HeaderOrder []string // noms des en-têtes dans l'ordre d'arrivée } // ParseH2ClientPreface extrait les paramètres SETTINGS et le WINDOW_UPDATE // depuis le flux HTTP/2 déchiffré du client. // data doit commencer APRÈS le magic preface (offset 24). func ParseH2ClientPreface(data []byte) (*HTTP2Settings, error) { settings := &HTTP2Settings{ HeaderTableSize: -1, EnablePush: -1, MaxConcurrentStreams: -1, InitialWindowSize: -1, MaxFrameSize: -1, MaxHeaderListSize: -1, UnknownSettings: -1, EnableConnectProtocol: -1, } offset := 0 // Parser au maximum 10 frames pour éviter une boucle infinie for frameIdx := 0; frameIdx < 10 && offset < len(data); frameIdx++ { if offset+9 > len(data) { break } hdr, err := parseH2FrameHeader(data[offset:]) if err != nil { break } offset += 9 payloadEnd := offset + int(hdr.Length) if payloadEnd > len(data) { break } payload := data[offset:payloadEnd] offset = payloadEnd switch hdr.Type { case h2FrameSettings: // Parser uniquement les SETTINGS du client (stream 0) if hdr.StreamID == 0 { pairs, err := parseH2SettingsFrame(payload) if err != nil { continue } for id, val := range pairs { switch id { case h2SettingHeaderTableSize: settings.HeaderTableSize = int32(val) case h2SettingEnablePush: settings.EnablePush = int32(val) case h2SettingMaxConcurrentStreams: settings.MaxConcurrentStreams = int32(val) case h2SettingInitialWindowSize: settings.InitialWindowSize = int32(val) case h2SettingMaxFrameSize: settings.MaxFrameSize = int32(val) case h2SettingMaxHeaderListSize: settings.MaxHeaderListSize = int32(val) case 7: // paramètre non standard (JA4H2) settings.UnknownSettings = int32(val) case h2SettingEnableConnectProtocol: settings.EnableConnectProtocol = int32(val) } } } case h2FrameWindowUpdate: // WINDOW_UPDATE sur stream 0 = flux de connexion if hdr.StreamID == 0 && len(payload) >= 4 { settings.WindowUpdateIncrement = binary.BigEndian.Uint32(payload[0:4]) & 0x7FFFFFFF } case h2FrameHeaders: // Extraire l'ordre des pseudo-headers et les en-têtes réguliers if hdr.StreamID > 0 && len(settings.PseudoHeaderOrder) == 0 { settings.PseudoHeaderOrder = ParseH2PseudoHeaders(payload) // Extraire aussi les en-têtes réguliers (User-Agent, Accept, etc.) kv, order := DecodeH2HeadersBlock(payload) if len(kv) > 0 { settings.HeaderKV = kv settings.HeaderOrder = order } } } } return settings, nil } // parseH2SettingsFrame extrait les paires (identifiant, valeur) d'une frame SETTINGS. // Chaque paire fait 6 octets : identifiant(2) + valeur(4). func parseH2SettingsFrame(payload []byte) (map[uint16]uint32, error) { if len(payload)%6 != 0 { return nil, fmt.Errorf("longueur de frame SETTINGS invalide: %d (doit être multiple de 6)", len(payload)) } result := make(map[uint16]uint32) for i := 0; i+6 <= len(payload); i += 6 { id := binary.BigEndian.Uint16(payload[i : i+2]) val := binary.BigEndian.Uint32(payload[i+2 : i+6]) result[id] = val } return result, nil } // ParseH2PseudoHeaders extrait l'ordre des pseudo-headers depuis un bloc HPACK. // // Implémentation simplifiée : détecte les pseudo-headers via les index HPACK statiques. // Table statique HPACK (RFC 7541, Annexe A) — index pertinents : // 1 :authority // 2 :method = GET // 3 :method = POST // 4 :path = / // 5 :path = /index.html // 6 :scheme = http // 7 :scheme = https func ParseH2PseudoHeaders(headersBlock []byte) []string { // Index HPACK statique → pseudo-header hpackStaticPseudo := map[int]string{ 1: ":authority", 2: ":method", 3: ":method", 4: ":path", 5: ":path", 6: ":scheme", 7: ":scheme", } seen := make(map[string]bool) var order []string offset := 0 for offset < len(headersBlock) { b := headersBlock[offset] // Représentation indexée (bit 7 = 1) : RFC 7541 §6.1 if b&0x80 != 0 { idx := int(b & 0x7F) if name, ok := hpackStaticPseudo[idx]; ok { if !seen[name] { seen[name] = true order = append(order, name) } } else if idx == 0 { // Fin de la liste d'index ou encodage multi-octets offset++ continue } else { // Index dynamique ou non-pseudo-header : arrêter le scan break } offset++ continue } // Représentation littérale avec index incrémental (bits 7-6 = 01) : RFC 7541 §6.2.1 if b&0xC0 == 0x40 { idx := int(b & 0x3F) if name, ok := hpackStaticPseudo[idx]; ok { if !seen[name] { seen[name] = true order = append(order, name) } } offset++ // Sauter la valeur (longueur + contenu) if offset >= len(headersBlock) { break } valueLen := int(headersBlock[offset] & 0x7F) // ignorer le bit Huffman offset += 1 + valueLen continue } // Tout autre encodage : arrêter (ce n'est probablement plus un pseudo-header) break } return order } // --------------------------------------------------------------------------- // HPACK static table (RFC 7541, Appendix A) — index → header name // Seuls les noms sont listés (les valeurs par défaut sont ignorées car // les en-têtes d'intérêt comme User-Agent sont toujours envoyés en littéral). // --------------------------------------------------------------------------- var hpackStaticTable = map[int]string{ 1: ":authority", 2: ":method", 3: ":method", 4: ":path", 5: ":path", 6: ":scheme", 7: ":scheme", 8: ":status", 9: ":status", 10: ":status", 11: ":status", 12: ":status", 13: ":status", 14: ":status", 15: "accept-encoding", 16: "accept-encoding", 17: "accept-language", 18: "cache-control", 19: "cookie", 20: "date", 21: "etag", 22: "if-modified-since", 23: "if-none-match", 24: "last-modified", 25: "link", 26: "location", 27: "referer", 28: "set-cookie", 29: ":method", 30: ":method", 31: ":method", 32: ":path", 33: ":scheme", 34: ":status", 35: "accept", 36: "accept", 37: "accept", 38: "accept-encoding", 39: "accept-encoding", 40: "accept-language", 41: "accept-language", 42: "access-control-allow-headers", 43: "access-control-allow-headers", 44: "access-control-allow-methods", 45: "access-control-allow-origin", 46: "access-control-request-headers", 47: "access-control-request-method", 48: "age", 49: "authorization", 50: "cache-control", 51: "content-disposition", 52: "content-encoding", 53: "content-length", 54: "content-location", 55: "content-range", 56: "content-type", 57: "content-type", 58: "cookie", 59: "date", 60: "etag", 61: "expect", 62: "expires", 63: "from", 64: "host", 65: "if-match", 66: "if-modified-since", 67: "if-none-match", 68: "if-range", 69: "if-unmodified-since", 70: "last-modified", 71: "link", 72: "location", 73: "max-forwards", 74: "proxy-authenticate", 75: "proxy-authorization", 76: "range", 77: "referer", 78: "refresh", 79: "retry-after", 80: "server", 81: "set-cookie", 82: "strict-transport-security", 83: "transfer-encoding", 84: "user-agent", 85: "user-agent", 86: "vary", 87: "vary", 88: "via", 89: "www-authenticate", 90: "x-forwarded-for", 91: "x-forwarded-proto", 92: "x-requested-with", 93: "sec-websocket-key", 94: "sec-ch-ua", 95: "user-agent", 96: "sec-ch-ua-mobile", 97: "sec-ch-ua-platform", 98: "sec-fetch-dest", 99: "sec-fetch-mode", 100: "sec-fetch-site", } // hpackCapturedHeaders est la liste des en-têtes H2 dont on capture la valeur. var hpackCapturedHeaders = map[string]bool{ "user-agent": true, "accept": true, "accept-encoding": true, "accept-language": true, "content-type": true, "x-request-id": true, "x-trace-id": true, "x-forwarded-for": true, "sec-ch-ua": true, "sec-ch-ua-mobile": true, "sec-ch-ua-platform": true, "sec-fetch-dest": true, "sec-fetch-mode": true, "sec-fetch-site": true, ":method": true, ":path": true, ":authority": true, ":scheme": true, "cookie": true, "referer": true, "host": true, } // hpackInteger décode un entier HPACK avec le préfixe spécifié (RFC 7541 §5.1). // Retourne la valeur décodée et le nombre d'octets consommés. func hpackInteger(data []byte, prefixBits int) (int, int) { if len(data) == 0 { return 0, 0 } mask := (1 << prefixBits) - 1 value := int(data[0] & byte(mask)) offset := 1 if value < mask { return value, offset } // Extension multi-octets m := 0 for offset < len(data) && offset < 6 { // limite de sécurité b := int(data[offset]) value += (b & 0x7F) << m m += 7 offset++ if b&0x80 == 0 { break } } return value, offset } // hpackString décode une chaîne HPACK (RFC 7541 §5.2). // Retourne la chaîne décodée et le nombre d'octets consommés. // Le décodage Huffman n'est pas implémenté — les chaînes Huffman sont ignorées. func hpackString(data []byte) (string, int) { if len(data) == 0 { return "", 0 } isHuffman := data[0]&0x80 != 0 length, offset := hpackInteger(data, 7) if isHuffman { // Huffman non implémenté — on ne peut pas décoder la valeur return "", offset + length } if offset+length > len(data) { // Données tronquées — retourner ce qu'on peut if offset < len(data) { return string(data[offset:]), len(data) } return "", offset } return string(data[offset : offset+length]), offset + length } // DecodeH2HeadersBlock décode un bloc d'en-têtes HPACK depuis un HEADERS frame. // Retourne un map nom→valeur et la liste ordonnée des noms. // Gère les représentations les plus courantes : // - Indexée (6.1) : index → nom+valeur de la table statique // - Littérale avec index incrémental (6.2.1) : nom indexé + valeur littérale // - Littérale sans indexation (6.2.2) : nom indexé + valeur littérale // - Littérale jamais indexée (6.2.3) : nom indexé + valeur littérale // - Nouveau nom littéral (6.2.x avec index=0) : nom littéral + valeur littérale func DecodeH2HeadersBlock(block []byte) (map[string]string, []string) { kv := make(map[string]string) var order []string offset := 0 for offset < len(block) && len(kv) < 50 { // limite de sécurité b := block[offset] // 1. Représentation indexée (bit 7 = 1) : RFC 7541 §6.1 if b&0x80 != 0 { idx, n := hpackInteger(block[offset:], 7) offset += n if idx > 0 && idx <= len(hpackStaticTable) { // Uniquement indexée — nom et valeur viennent de la table // Pour les entrées "nom uniquement" (pas de valeur par défaut), // on ne peut pas extraire la valeur sans table dynamique _ = hpackStaticTable[idx] } continue } var name string var nameLen int // 2. Littérale avec index incrémental (bits 7-6 = 01) : RFC 7541 §6.2.1 if b&0xC0 == 0x40 { idx, n := hpackInteger(block[offset:], 6) offset += n if idx == 0 { // Nouveau nom : nom littéral suivi de valeur littérale name, nameLen = hpackString(block[offset:]) offset += nameLen } else if idx <= len(hpackStaticTable) { name = hpackStaticTable[idx] } value, valueLen := hpackString(block[offset:]) offset += valueLen nameLower := strings.ToLower(name) if nameLower != "" && value != "" && hpackCapturedHeaders[nameLower] { kv[nameLower] = value order = append(order, nameLower) } continue } // 3. Littérale sans indexation (bits 7-5 = 000) : RFC 7541 §6.2.2 if b&0xF0 == 0x00 { idx, n := hpackInteger(block[offset:], 4) offset += n if idx == 0 { name, nameLen = hpackString(block[offset:]) offset += nameLen } else if idx <= len(hpackStaticTable) { name = hpackStaticTable[idx] } value, valueLen := hpackString(block[offset:]) offset += valueLen nameLower := strings.ToLower(name) if nameLower != "" && value != "" && hpackCapturedHeaders[nameLower] { kv[nameLower] = value order = append(order, nameLower) } continue } // 4. Littérale jamais indexée (bits 7-5 = 0001) : RFC 7541 §6.2.3 if b&0xF0 == 0x10 { idx, n := hpackInteger(block[offset:], 4) offset += n if idx == 0 { name, nameLen = hpackString(block[offset:]) offset += nameLen } else if idx <= len(hpackStaticTable) { name = hpackStaticTable[idx] } value, valueLen := hpackString(block[offset:]) offset += valueLen nameLower := strings.ToLower(name) if nameLower != "" && value != "" && hpackCapturedHeaders[nameLower] { kv[nameLower] = value order = append(order, nameLower) } continue } // Représentation inconnue — arrêter break } return kv, order } // IsH2FrameHeader vérifie si les données commencent par un en-tête de frame HTTP/2 valide. // Utilisé pour détecter les frames H2 seules (sans préface) dans les SSL_read ultérieurs. func IsH2FrameHeader(data []byte) bool { if len(data) < 9 { return false } hdr, err := parseH2FrameHeader(data) if err != nil { return false } // Vérifications de plausibilité : // - Longueur ≤ 16384 (16 KiB, limite conservatrice pour un seul read) // - Type dans la plage 0-9 (types de frame définis) // - Stream ID dans une plage raisonnable if hdr.Length > 16384 { return false } if hdr.Type > 9 { return false } return true } // ExtractH2HeaderKV extrait les en-têtes des frames HEADERS HTTP/2. // Parcourt toutes les frames dans les données et décode les blocs HEADERS. func ExtractH2HeaderKV(data []byte) map[string]string { kv := make(map[string]string) offset := 0 for offset < len(data) && len(kv) < 50 { if offset+9 > len(data) { break } hdr, err := parseH2FrameHeader(data[offset:]) if err != nil { break } offset += 9 payloadEnd := offset + int(hdr.Length) if payloadEnd > len(data) { break } payload := data[offset:payloadEnd] offset = payloadEnd if hdr.Type == h2FrameHeaders && hdr.StreamID > 0 { frameKV, _ := DecodeH2HeadersBlock(payload) for k, v := range frameKV { if _, exists := kv[k]; !exists { kv[k] = v } } } } return kv }