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). // --------------------------------------------------------------------------- // hpackStaticEntry est une entrée de la table statique HPACK (RFC 7541 Appendix A). type hpackStaticEntry struct { Name string Value string } // hpackStaticTable est la table statique HPACK (RFC 7541 Appendix A). // Index 1-61 : RFC 7541 original. Index 62-100 : extensions RFC 9204 + navigateurs. var hpackStaticTable = map[int]hpackStaticEntry{ 1: {":authority", ""}, 2: {":method", "GET"}, 3: {":method", "POST"}, 4: {":path", "/"}, 5: {":path", "/index.html"}, 6: {":scheme", "http"}, 7: {":scheme", "https"}, 8: {":status", "200"}, 9: {":status", "204"}, 10: {":status", "206"}, 11: {":status", "304"}, 12: {":status", "400"}, 13: {":status", "404"}, 14: {":status", "500"}, 15: {"accept-charset", ""}, 16: {"accept-encoding", "gzip, deflate"}, 17: {"accept-language", ""}, 18: {"accept", ""}, 19: {"accept", "*/*"}, 20: {"access-control-allow-origin", ""}, 21: {"accept-encoding", ""}, 22: {"accept-encoding", "gzip, deflate"}, 23: {"accept-language", ""}, 24: {"accept-language", ""}, 25: {"access-control-allow-credentials", ""}, 26: {"access-control-allow-headers", ""}, 27: {"access-control-allow-methods", ""}, 28: {"access-control-allow-origin", ""}, 29: {"access-control-request-headers", ""}, 30: {"access-control-request-method", ""}, 31: {"age", ""}, 32: {"authorization", ""}, 33: {"cache-control", ""}, 34: {"cache-control", "max-age=0"}, 35: {"cookie", ""}, 36: {"cookie", ""}, 37: {"date", ""}, 38: {"etag", ""}, 39: {"expect", ""}, 40: {"from", ""}, 41: {"host", ""}, 42: {"if-match", ""}, 43: {"if-modified-since", ""}, 44: {"if-none-match", ""}, 45: {"if-range", ""}, 46: {"if-unmodified-since", ""}, 47: {"last-modified", ""}, 48: {"link", ""}, 49: {"location", ""}, 50: {"max-forwards", ""}, 51: {"proxy-authenticate", ""}, 52: {"proxy-authorization", ""}, 53: {"range", ""}, 54: {"referer", ""}, 55: {"refresh", ""}, 56: {"retry-after", ""}, 57: {"server", ""}, 58: {"set-cookie", ""}, 59: {"strict-transport-security", ""}, 60: {"transfer-encoding", ""}, 61: {"user-agent", ""}, 62: {"vary", ""}, 63: {"vary", "Accept-Encoding"}, 64: {"via", ""}, 65: {"www-authenticate", ""}, 66: {"x-forwarded-for", ""}, 67: {"x-forwarded-proto", ""}, 68: {"x-requested-with", ""}, 69: {"sec-websocket-key", ""}, 70: {"sec-websocket-version", ""}, 71: {"te", ""}, 72: {"upgrade", ""}, 73: {"sec-ch-ua", ""}, 74: {"sec-ch-ua-mobile", "?0"}, 75: {"sec-ch-ua-platform", ""}, 76: {"sec-fetch-dest", ""}, 77: {"sec-fetch-mode", ""}, 78: {"sec-fetch-site", ""}, 79: {"sec-fetch-user", "?1"}, 80: {"priority", ""}, 81: {"accept", ""}, 82: {"accept", "application/dns-message"}, 83: {"accept-language", ""}, 84: {":method", "CONNECT"}, 85: {":method", "DELETE"}, 86: {":method", "HEAD"}, 87: {":method", "OPTIONS"}, 88: {":method", "PATCH"}, 89: {":method", "PUT"}, 90: {":method", "TRACE"}, 91: {":path", "/"}, 92: {":path", "/0"}, 93: {":path", "/1"}, 94: {":path", "/2"}, 95: {":path", "/3"}, 96: {":path", "/4"}, 97: {":path", "/5"}, 98: {":path", "/6"}, 99: {":path", "/7"}, 100: {":path", "/8"}, } // 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] // will be replaced } 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].Name } 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].Name } 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].Name } 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 }