Created
July 30, 2025 19:38
-
-
Save wv-jessejjohnson/0f36f0be72421be4141fc14a04ebd327 to your computer and use it in GitHub Desktop.
HTTP2 Fingerprinting
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package fingerprint | |
| import ( | |
| "crypto/tls" | |
| "fmt" | |
| "net" | |
| "net/http" | |
| "strings" | |
| "golang.org/x/net/http2" | |
| ) | |
| // HTTP2Fingerprinter handles HTTP/2 protocol fingerprinting | |
| type HTTP2Fingerprinter struct { | |
| profiles map[string]*HTTP2Profile | |
| } | |
| type HTTP2Profile struct { | |
| Name string | |
| WindowUpdate uint32 | |
| HeaderTableSize uint32 | |
| EnablePush bool | |
| MaxConcurrentStreams uint32 | |
| InitialWindowSize uint32 | |
| MaxFrameSize uint32 | |
| MaxHeaderListSize uint32 | |
| PriorityFrames bool | |
| PseudoHeaderOrder []string | |
| HeaderOrder []string | |
| ConnectionFlow uint32 | |
| PriorityWeights map[int]uint8 | |
| PriorityDependencies map[int]int | |
| } | |
| func NewHTTP2Fingerprinter() *HTTP2Fingerprinter { | |
| fp := &HTTP2Fingerprinter{ | |
| profiles: make(map[string]*HTTP2Profile), | |
| } | |
| fp.initializeProfiles() | |
| return fp | |
| } | |
| func (h2fp *HTTP2Fingerprinter) initializeProfiles() { | |
| // Chrome HTTP/2 profile | |
| h2fp.profiles["chrome"] = &HTTP2Profile{ | |
| Name: "chrome", | |
| WindowUpdate: 15663105, | |
| HeaderTableSize: 65536, | |
| EnablePush: false, // Chrome disables server push | |
| MaxConcurrentStreams: 1000, | |
| InitialWindowSize: 6291456, | |
| MaxFrameSize: 16384, | |
| MaxHeaderListSize: 262144, | |
| PriorityFrames: true, | |
| PseudoHeaderOrder: []string{ | |
| ":method", | |
| ":authority", | |
| ":scheme", | |
| ":path", | |
| }, | |
| HeaderOrder: []string{ | |
| "cache-control", | |
| "sec-ch-ua", | |
| "sec-ch-ua-mobile", | |
| "sec-ch-ua-platform", | |
| "upgrade-insecure-requests", | |
| "user-agent", | |
| "accept", | |
| "sec-fetch-site", | |
| "sec-fetch-mode", | |
| "sec-fetch-user", | |
| "sec-fetch-dest", | |
| "accept-encoding", | |
| "accept-language", | |
| }, | |
| ConnectionFlow: 15728640, | |
| PriorityWeights: map[int]uint8{ | |
| 0: 255, // Root stream | |
| 1: 220, // Main document | |
| 2: 110, // CSS/JS | |
| 3: 55, // Images | |
| }, | |
| PriorityDependencies: map[int]int{ | |
| 1: 0, // Main depends on root | |
| 2: 1, // Resources depend on main | |
| 3: 1, // Images depend on main | |
| }, | |
| } | |
| // Firefox HTTP/2 profile | |
| h2fp.profiles["firefox"] = &HTTP2Profile{ | |
| Name: "firefox", | |
| WindowUpdate: 12517377, | |
| HeaderTableSize: 65536, | |
| EnablePush: true, // Firefox supports server push | |
| MaxConcurrentStreams: 1000, | |
| InitialWindowSize: 131072, | |
| MaxFrameSize: 16384, | |
| MaxHeaderListSize: 262144, | |
| PriorityFrames: true, | |
| PseudoHeaderOrder: []string{ | |
| ":method", | |
| ":path", | |
| ":authority", | |
| ":scheme", | |
| }, | |
| HeaderOrder: []string{ | |
| "user-agent", | |
| "accept", | |
| "accept-language", | |
| "accept-encoding", | |
| "dnt", | |
| "connection", | |
| "upgrade-insecure-requests", | |
| }, | |
| ConnectionFlow: 12517377, | |
| PriorityWeights: map[int]uint8{ | |
| 0: 255, | |
| 1: 200, | |
| 2: 100, | |
| 3: 50, | |
| }, | |
| PriorityDependencies: map[int]int{ | |
| 1: 0, | |
| 2: 1, | |
| 3: 1, | |
| }, | |
| } | |
| // Safari HTTP/2 profile | |
| h2fp.profiles["safari"] = &HTTP2Profile{ | |
| Name: "safari", | |
| WindowUpdate: 2097152, | |
| HeaderTableSize: 4096, | |
| EnablePush: true, | |
| MaxConcurrentStreams: 100, | |
| InitialWindowSize: 2097152, | |
| MaxFrameSize: 16384, | |
| MaxHeaderListSize: 0, // Safari doesn't set this | |
| PriorityFrames: false, // Safari doesn't use priority frames | |
| PseudoHeaderOrder: []string{ | |
| ":method", | |
| ":scheme", | |
| ":path", | |
| ":authority", | |
| }, | |
| HeaderOrder: []string{ | |
| "accept", | |
| "accept-encoding", | |
| "accept-language", | |
| "user-agent", | |
| "connection", | |
| }, | |
| ConnectionFlow: 1048576, | |
| PriorityWeights: map[int]uint8{ | |
| 0: 255, | |
| }, | |
| PriorityDependencies: map[int]int{}, | |
| } | |
| // Edge HTTP/2 profile (similar to Chrome but with differences) | |
| h2fp.profiles["edge"] = &HTTP2Profile{ | |
| Name: "edge", | |
| WindowUpdate: 15663105, | |
| HeaderTableSize: 65536, | |
| EnablePush: false, | |
| MaxConcurrentStreams: 1000, | |
| InitialWindowSize: 6291456, | |
| MaxFrameSize: 16384, | |
| MaxHeaderListSize: 262144, | |
| PriorityFrames: true, | |
| PseudoHeaderOrder: []string{ | |
| ":method", | |
| ":authority", | |
| ":scheme", | |
| ":path", | |
| }, | |
| HeaderOrder: []string{ | |
| "cache-control", | |
| "sec-ch-ua", | |
| "sec-ch-ua-mobile", | |
| "sec-ch-ua-platform", | |
| "upgrade-insecure-requests", | |
| "user-agent", | |
| "accept", | |
| "sec-fetch-site", | |
| "sec-fetch-mode", | |
| "sec-fetch-user", | |
| "sec-fetch-dest", | |
| "accept-encoding", | |
| "accept-language", | |
| }, | |
| ConnectionFlow: 15728640, | |
| PriorityWeights: map[int]uint8{ | |
| 0: 255, | |
| 1: 220, | |
| 2: 110, | |
| 3: 55, | |
| }, | |
| PriorityDependencies: map[int]int{ | |
| 1: 0, | |
| 2: 1, | |
| 3: 1, | |
| }, | |
| } | |
| } | |
| func (h2fp *HTTP2Fingerprinter) ConfigureTransport(transport *http.Transport, profileName string) error { | |
| profile, exists := h2fp.profiles[profileName] | |
| if !exists { | |
| return fmt.Errorf("HTTP/2 profile %s not found", profileName) | |
| } | |
| // Configure HTTP/2 transport | |
| http2Transport := &http2.Transport{ | |
| TLSClientConfig: transport.TLSClientConfig, | |
| AllowHTTP: false, | |
| DialTLS: func(network, addr string, cfg *tls.Config) (net.Conn, error) { | |
| return tls.Dial(network, addr, cfg) | |
| }, | |
| } | |
| // Apply HTTP/2 specific settings | |
| if err := h2fp.configureHTTP2Settings(http2Transport, profile); err != nil { | |
| return fmt.Errorf("failed to configure HTTP/2 settings: %w", err) | |
| } | |
| // Replace the transport's HTTP/2 configuration | |
| if err := http2.ConfigureTransport(transport); err != nil { | |
| return fmt.Errorf("failed to configure HTTP/2 transport: %w", err) | |
| } | |
| return nil | |
| } | |
| func (h2fp *HTTP2Fingerprinter) configureHTTP2Settings(transport *http2.Transport, profile *HTTP2Profile) error { | |
| // Note: The http2.Transport in Go's standard library doesn't expose | |
| // all the settings we need for complete fingerprinting. | |
| // In a production implementation, you might need to use a custom | |
| // HTTP/2 implementation or fork the standard library. | |
| // Configure what we can through the available API | |
| transport.MaxHeaderListSize = profile.MaxHeaderListSize | |
| transport.StrictMaxConcurrentStreams = true | |
| // For more advanced HTTP/2 fingerprinting, we would need to: | |
| // 1. Implement custom frame writers | |
| // 2. Control SETTINGS frame parameters | |
| // 3. Manage stream priorities | |
| // 4. Control window update behavior | |
| return nil | |
| } | |
| func (h2fp *HTTP2Fingerprinter) ApplyHTTP2Headers(req *http.Request, profileName string) error { | |
| profile, exists := h2fp.profiles[profileName] | |
| if !exists { | |
| return fmt.Errorf("HTTP/2 profile %s not found", profileName) | |
| } | |
| // Reorder headers according to profile | |
| h2fp.reorderHeaders(req, profile) | |
| // Add HTTP/2 specific headers | |
| h2fp.addHTTP2SpecificHeaders(req, profile) | |
| return nil | |
| } | |
| func (h2fp *HTTP2Fingerprinter) reorderHeaders(req *http.Request, profile *HTTP2Profile) { | |
| // Store original headers | |
| originalHeaders := make(map[string][]string) | |
| for key, values := range req.Header { | |
| originalHeaders[strings.ToLower(key)] = values | |
| } | |
| // Clear existing headers | |
| req.Header = make(http.Header) | |
| // Add headers in the specified order | |
| for _, headerName := range profile.HeaderOrder { | |
| if values, exists := originalHeaders[strings.ToLower(headerName)]; exists { | |
| for _, value := range values { | |
| req.Header.Add(headerName, value) | |
| } | |
| delete(originalHeaders, strings.ToLower(headerName)) | |
| } | |
| } | |
| // Add any remaining headers | |
| for key, values := range originalHeaders { | |
| for _, value := range values { | |
| req.Header.Add(key, value) | |
| } | |
| } | |
| } | |
| func (h2fp *HTTP2Fingerprinter) addHTTP2SpecificHeaders(req *http.Request, profile *HTTP2Profile) { | |
| // Add browser-specific HTTP/2 headers | |
| switch profile.Name { | |
| case "chrome", "edge": | |
| // Chrome/Edge specific headers are already handled in browser fingerprinting | |
| break | |
| case "firefox": | |
| // Firefox doesn't send as many proprietary headers | |
| break | |
| case "safari": | |
| // Safari has minimal additional headers | |
| break | |
| } | |
| } | |
| // Advanced HTTP/2 fingerprinting with custom framing | |
| type AdvancedHTTP2Fingerprinter struct { | |
| frameWriters map[string]FrameWriter | |
| } | |
| type FrameWriter interface { | |
| WriteSettings(settings []http2.Setting) error | |
| WriteWindowUpdate(streamID uint32, increment uint32) error | |
| WritePriority(streamID uint32, priority http2.PriorityParam) error | |
| } | |
| type HTTP2SettingsFrame struct { | |
| HeaderTableSize *uint32 | |
| EnablePush *bool | |
| MaxConcurrentStreams *uint32 | |
| InitialWindowSize *uint32 | |
| MaxFrameSize *uint32 | |
| MaxHeaderListSize *uint32 | |
| } | |
| func NewAdvancedHTTP2Fingerprinter() *AdvancedHTTP2Fingerprinter { | |
| return &AdvancedHTTP2Fingerprinter{ | |
| frameWriters: make(map[string]FrameWriter), | |
| } | |
| } | |
| func (ah2fp *AdvancedHTTP2Fingerprinter) CreateSettingsFrame(profileName string) (*HTTP2SettingsFrame, error) { | |
| // This would create a custom SETTINGS frame based on the browser profile | |
| // Implementation would require low-level HTTP/2 frame manipulation | |
| switch profileName { | |
| case "chrome": | |
| return &HTTP2SettingsFrame{ | |
| HeaderTableSize: uint32Ptr(65536), | |
| EnablePush: boolPtr(false), | |
| MaxConcurrentStreams: uint32Ptr(1000), | |
| InitialWindowSize: uint32Ptr(6291456), | |
| MaxFrameSize: uint32Ptr(16384), | |
| MaxHeaderListSize: uint32Ptr(262144), | |
| }, nil | |
| case "firefox": | |
| return &HTTP2SettingsFrame{ | |
| HeaderTableSize: uint32Ptr(65536), | |
| EnablePush: boolPtr(true), | |
| MaxConcurrentStreams: uint32Ptr(1000), | |
| InitialWindowSize: uint32Ptr(131072), | |
| MaxFrameSize: uint32Ptr(16384), | |
| MaxHeaderListSize: uint32Ptr(262144), | |
| }, nil | |
| case "safari": | |
| return &HTTP2SettingsFrame{ | |
| HeaderTableSize: uint32Ptr(4096), | |
| EnablePush: boolPtr(true), | |
| MaxConcurrentStreams: uint32Ptr(100), | |
| InitialWindowSize: uint32Ptr(2097152), | |
| MaxFrameSize: uint32Ptr(16384), | |
| // Safari doesn't set MaxHeaderListSize | |
| }, nil | |
| default: | |
| return nil, fmt.Errorf("unknown profile: %s", profileName) | |
| } | |
| } | |
| // HTTP/2 ALPN and protocol negotiation fingerprinting | |
| type ALPNFingerprinter struct { | |
| profiles map[string][]string | |
| } | |
| func NewALPNFingerprinter() *ALPNFingerprinter { | |
| af := &ALPNFingerprinter{ | |
| profiles: make(map[string][]string), | |
| } | |
| // Chrome ALPN negotiation | |
| af.profiles["chrome"] = []string{"h2", "http/1.1"} | |
| // Firefox ALPN negotiation | |
| af.profiles["firefox"] = []string{"h2", "http/1.1"} | |
| // Safari ALPN negotiation | |
| af.profiles["safari"] = []string{"h2", "http/1.1"} | |
| // Edge ALPN negotiation | |
| af.profiles["edge"] = []string{"h2", "http/1.1"} | |
| return af | |
| } | |
| func (af *ALPNFingerprinter) ConfigureTLSALPN(tlsConfig *tls.Config, profileName string) { | |
| if protocols, exists := af.profiles[profileName]; exists { | |
| tlsConfig.NextProtos = protocols | |
| } else { | |
| // Default to HTTP/2 and HTTP/1.1 | |
| tlsConfig.NextProtos = []string{"h2", "http/1.1"} | |
| } | |
| } | |
| // HTTP/2 connection preface and initial frames | |
| type HTTP2ConnectionFingerprinter struct { | |
| connectionPrefaceOrder map[string][]string | |
| } | |
| func NewHTTP2ConnectionFingerprinter() *HTTP2ConnectionFingerprinter { | |
| hcf := &HTTP2ConnectionFingerprinter{ | |
| connectionPrefaceOrder: make(map[string][]string), | |
| } | |
| // Define the order of initial frames for different browsers | |
| hcf.connectionPrefaceOrder["chrome"] = []string{ | |
| "SETTINGS", | |
| "WINDOW_UPDATE", | |
| "HEADERS", // First request | |
| } | |
| hcf.connectionPrefaceOrder["firefox"] = []string{ | |
| "SETTINGS", | |
| "WINDOW_UPDATE", | |
| "PRIORITY", | |
| "HEADERS", | |
| } | |
| hcf.connectionPrefaceOrder["safari"] = []string{ | |
| "SETTINGS", | |
| "WINDOW_UPDATE", | |
| "HEADERS", | |
| } | |
| return hcf | |
| } | |
| func (hcf *HTTP2ConnectionFingerprinter) GetInitialFrameOrder(profileName string) []string { | |
| if order, exists := hcf.connectionPrefaceOrder[profileName]; exists { | |
| return order | |
| } | |
| return hcf.connectionPrefaceOrder["chrome"] // Default | |
| } | |
| // JA3 fingerprinting integration for HTTP/2 | |
| type JA3HTTP2Fingerprinter struct { | |
| ja3Profiles map[string]string | |
| } | |
| func NewJA3HTTP2Fingerprinter() *JA3HTTP2Fingerprinter { | |
| return &JA3HTTP2Fingerprinter{ | |
| ja3Profiles: map[string]string{ | |
| "chrome": "771,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,0-23-65281-10-11-35-16-5-13-18-51-45-43-27-21,29-23-24,0", | |
| "firefox": "771,4865-4867-4866-49195-49199-52393-52392-49196-49200-49162-49161-49171-49172-51-57-47-53,0-23-65281-10-11-35-16-5-51-43-13-45-28-21,29-23-24-25,0", | |
| "safari": "771,4865-4866-4867-49196-49195-52393-49200-49199-52392-49162-49161-49172-49171-157-156-53-47,65281-0-23-35-13-5-18-16-30032-11-10,29-23-24,0", | |
| }, | |
| } | |
| } | |
| func (jhf *JA3HTTP2Fingerprinter) GetJA3Profile(profileName string) string { | |
| if ja3, exists := jhf.ja3Profiles[profileName]; exists { | |
| return ja3 | |
| } | |
| return jhf.ja3Profiles["chrome"] // Default | |
| } | |
| // Utility functions | |
| func uint32Ptr(v uint32) *uint32 { | |
| return &v | |
| } | |
| func boolPtr(v bool) *bool { | |
| return &v | |
| } | |
| // HTTP/2 stream dependency and priority fingerprinting | |
| type StreamPriorityFingerprinter struct { | |
| priorityTrees map[string]*PriorityTree | |
| } | |
| type PriorityTree struct { | |
| Nodes map[int]*PriorityNode | |
| } | |
| type PriorityNode struct { | |
| StreamID int | |
| ParentID int | |
| Weight uint8 | |
| Exclusive bool | |
| Children []int | |
| } | |
| func NewStreamPriorityFingerprinter() *StreamPriorityFingerprinter { | |
| spf := &StreamPriorityFingerprinter{ | |
| priorityTrees: make(map[string]*PriorityTree), | |
| } | |
| // Chrome priority tree | |
| spf.priorityTrees["chrome"] = &PriorityTree{ | |
| Nodes: map[int]*PriorityNode{ | |
| 0: {StreamID: 0, ParentID: -1, Weight: 255, Exclusive: false}, | |
| 1: {StreamID: 1, ParentID: 0, Weight: 220, Exclusive: false}, // Main document | |
| 3: {StreamID: 3, ParentID: 0, Weight: 200, Exclusive: false}, // High priority resources | |
| 5: {StreamID: 5, ParentID: 0, Weight: 110, Exclusive: false}, // CSS/JS | |
| 7: {StreamID: 7, ParentID: 0, Weight: 90, Exclusive: false}, // Images | |
| }, | |
| } | |
| // Firefox priority tree (simpler structure) | |
| spf.priorityTrees["firefox"] = &PriorityTree{ | |
| Nodes: map[int]*PriorityNode{ | |
| 0: {StreamID: 0, ParentID: -1, Weight: 255, Exclusive: false}, | |
| 1: {StreamID: 1, ParentID: 0, Weight: 200, Exclusive: false}, | |
| 3: {StreamID: 3, ParentID: 1, Weight: 100, Exclusive: false}, | |
| }, | |
| } | |
| return spf | |
| } | |
| func (spf *StreamPriorityFingerprinter) GetPriorityTree(profileName string) *PriorityTree { | |
| if tree, exists := spf.priorityTrees[profileName]; exists { | |
| return tree | |
| } | |
| return spf.priorityTrees["chrome"] // Default | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment