| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307 |
- package splithttp
- import (
- "crypto/rand"
- "math"
- "net/http"
- "net/url"
- "strings"
- "golang.org/x/net/http2/hpack"
- )
- type PaddingMethod string
- const (
- PaddingMethodRepeatX PaddingMethod = "repeat-x"
- PaddingMethodTokenish PaddingMethod = "tokenish"
- )
- const charsetBase62 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
- // Huffman encoding gives ~20% size reduction for base62 sequences
- const avgHuffmanBytesPerCharBase62 = 0.8
- const validationTolerance = 2
- type XPaddingPlacement struct {
- Placement string
- Key string
- Header string
- RawURL string
- }
- type XPaddingConfig struct {
- Length int
- Placement XPaddingPlacement
- Method PaddingMethod
- }
- func randStringFromCharset(n int, charset string) (string, bool) {
- if n <= 0 || len(charset) == 0 {
- return "", false
- }
- m := len(charset)
- limit := byte(256 - (256 % m))
- result := make([]byte, n)
- i := 0
- buf := make([]byte, 256)
- for i < n {
- if _, err := rand.Read(buf); err != nil {
- return "", false
- }
- for _, rb := range buf {
- if rb >= limit {
- continue
- }
- result[i] = charset[int(rb)%m]
- i++
- if i == n {
- break
- }
- }
- }
- return string(result), true
- }
- func absInt(x int) int {
- if x < 0 {
- return -x
- }
- return x
- }
- func GenerateTokenishPaddingBase62(targetHuffmanBytes int) string {
- n := int(math.Ceil(float64(targetHuffmanBytes) / avgHuffmanBytesPerCharBase62))
- if n < 1 {
- n = 1
- }
- randBase62Str, ok := randStringFromCharset(n, charsetBase62)
- if !ok {
- return ""
- }
- const maxIter = 150
- adjustChar := byte('X')
- // Adjust until close enough
- for iter := 0; iter < maxIter; iter++ {
- currentLength := int(hpack.HuffmanEncodeLength(randBase62Str))
- diff := currentLength - targetHuffmanBytes
- if absInt(diff) <= validationTolerance {
- return randBase62Str
- }
- if diff < 0 {
- // Too small -> append padding char(s)
- randBase62Str += string(adjustChar)
- // Avoid a long run of identical chars
- if adjustChar == 'X' {
- adjustChar = 'Z'
- } else {
- adjustChar = 'X'
- }
- } else {
- // Too big -> remove from the end
- if len(randBase62Str) <= 1 {
- return randBase62Str
- }
- randBase62Str = randBase62Str[:len(randBase62Str)-1]
- }
- }
- return randBase62Str
- }
- func GeneratePadding(method PaddingMethod, length int) string {
- if length <= 0 {
- return ""
- }
- // https://www.rfc-editor.org/rfc/rfc7541.html#appendix-B
- // h2's HPACK Header Compression feature employs a huffman encoding using a static table.
- // 'X' and 'Z' are assigned an 8 bit code, so HPACK compression won't change actual padding length on the wire.
- // https://www.rfc-editor.org/rfc/rfc9204.html#section-4.1.2-2
- // h3's similar QPACK feature uses the same huffman table.
- switch method {
- case PaddingMethodRepeatX:
- return strings.Repeat("X", length)
- case PaddingMethodTokenish:
- paddingValue := GenerateTokenishPaddingBase62(length)
- if paddingValue == "" {
- return strings.Repeat("X", length)
- }
- return paddingValue
- default:
- return strings.Repeat("X", length)
- }
- }
- func ApplyPaddingToCookie(req *http.Request, name, value string) {
- if req == nil || name == "" || value == "" {
- return
- }
- req.AddCookie(&http.Cookie{
- Name: name,
- Value: value,
- Path: "/",
- })
- }
- func ApplyPaddingToQuery(u *url.URL, key, value string) {
- if u == nil || key == "" || value == "" {
- return
- }
- q := u.Query()
- q.Set(key, value)
- u.RawQuery = q.Encode()
- }
- func (c *Config) GetNormalizedXPaddingBytes() RangeConfig {
- if c.XPaddingBytes == nil || c.XPaddingBytes.To == 0 {
- return RangeConfig{
- From: 100,
- To: 1000,
- }
- }
- return *c.XPaddingBytes
- }
- func (c *Config) ApplyXPaddingToHeader(h http.Header, config XPaddingConfig) {
- if h == nil {
- return
- }
- paddingValue := GeneratePadding(config.Method, config.Length)
- switch p := config.Placement; p.Placement {
- case PlacementHeader:
- h.Set(p.Header, paddingValue)
- case PlacementQueryInHeader:
- u, err := url.Parse(p.RawURL)
- if err != nil || u == nil {
- return
- }
- u.RawQuery = p.Key + "=" + paddingValue
- h.Set(p.Header, u.String())
- }
- }
- func (c *Config) ApplyXPaddingToRequest(req *http.Request, config XPaddingConfig) {
- if req == nil {
- return
- }
- if req.Header == nil {
- req.Header = make(http.Header)
- }
- placement := config.Placement.Placement
- if placement == PlacementHeader || placement == PlacementQueryInHeader {
- c.ApplyXPaddingToHeader(req.Header, config)
- return
- }
- paddingValue := GeneratePadding(config.Method, config.Length)
- switch placement {
- case PlacementCookie:
- ApplyPaddingToCookie(req, config.Placement.Key, paddingValue)
- case PlacementQuery:
- ApplyPaddingToQuery(req.URL, config.Placement.Key, paddingValue)
- }
- }
- func (c *Config) ExtractXPaddingFromRequest(req *http.Request, obfsMode bool) (string, string) {
- if req == nil {
- return "", ""
- }
- if !obfsMode {
- referrer := req.Header.Get("Referer")
- if referrer != "" {
- if referrerURL, err := url.Parse(referrer); err == nil {
- paddingValue := referrerURL.Query().Get("x_padding")
- paddingPlacement := PlacementQueryInHeader + "=Referer, key=x_padding"
- return paddingValue, paddingPlacement
- }
- } else {
- paddingValue := req.URL.Query().Get("x_padding")
- return paddingValue, PlacementQuery + ", key=x_padding"
- }
- }
- key := c.XPaddingKey
- header := c.XPaddingHeader
- if cookie, err := req.Cookie(key); err == nil {
- if cookie != nil && cookie.Value != "" {
- paddingValue := cookie.Value
- paddingPlacement := PlacementCookie + ", key=" + key
- return paddingValue, paddingPlacement
- }
- }
- headerValue := req.Header.Get(header)
- if headerValue != "" {
- if c.XPaddingPlacement == PlacementHeader {
- paddingPlacement := PlacementHeader + "=" + header
- return headerValue, paddingPlacement
- }
- if parsedURL, err := url.Parse(headerValue); err == nil {
- paddingPlacement := PlacementQueryInHeader + "=" + header + ", key=" + key
- return parsedURL.Query().Get(key), paddingPlacement
- }
- }
- queryValue := req.URL.Query().Get(key)
- if queryValue != "" {
- paddingPlacement := PlacementQuery + ", key=" + key
- return queryValue, paddingPlacement
- }
- return "", ""
- }
- func (c *Config) IsPaddingValid(paddingValue string, from, to int32, method PaddingMethod) bool {
- if paddingValue == "" {
- return false
- }
- if to <= 0 {
- r := c.GetNormalizedXPaddingBytes()
- from, to = r.From, r.To
- }
- switch method {
- case PaddingMethodRepeatX:
- n := int32(len(paddingValue))
- return n >= from && n <= to
- case PaddingMethodTokenish:
- const tolerance = int32(validationTolerance)
- n := int32(hpack.HuffmanEncodeLength(paddingValue))
- f := from - tolerance
- t := to + tolerance
- if f < 0 {
- f = 0
- }
- return n >= f && n <= t
- default:
- n := int32(len(paddingValue))
- return n >= from && n <= to
- }
- }
|