Skip to content

Instantly share code, notes, and snippets.

@wv-jessejjohnson
Created July 30, 2025 19:38
Show Gist options
  • Select an option

  • Save wv-jessejjohnson/0f36f0be72421be4141fc14a04ebd327 to your computer and use it in GitHub Desktop.

Select an option

Save wv-jessejjohnson/0f36f0be72421be4141fc14a04ebd327 to your computer and use it in GitHub Desktop.
HTTP2 Fingerprinting
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