xpadding.go 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  1. package splithttp
  2. import (
  3. "crypto/rand"
  4. "math"
  5. "net/http"
  6. "net/url"
  7. "strings"
  8. "golang.org/x/net/http2/hpack"
  9. )
  10. type PaddingMethod string
  11. const (
  12. PaddingMethodRepeatX PaddingMethod = "repeat-x"
  13. PaddingMethodTokenish PaddingMethod = "tokenish"
  14. )
  15. const charsetBase62 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
  16. // Huffman encoding gives ~20% size reduction for base62 sequences
  17. const avgHuffmanBytesPerCharBase62 = 0.8
  18. const validationTolerance = 2
  19. type XPaddingPlacement struct {
  20. Placement string
  21. Key string
  22. Header string
  23. RawURL string
  24. }
  25. type XPaddingConfig struct {
  26. Length int
  27. Placement XPaddingPlacement
  28. Method PaddingMethod
  29. }
  30. func randStringFromCharset(n int, charset string) (string, bool) {
  31. if n <= 0 || len(charset) == 0 {
  32. return "", false
  33. }
  34. m := len(charset)
  35. limit := byte(256 - (256 % m))
  36. result := make([]byte, n)
  37. i := 0
  38. buf := make([]byte, 256)
  39. for i < n {
  40. if _, err := rand.Read(buf); err != nil {
  41. return "", false
  42. }
  43. for _, rb := range buf {
  44. if rb >= limit {
  45. continue
  46. }
  47. result[i] = charset[int(rb)%m]
  48. i++
  49. if i == n {
  50. break
  51. }
  52. }
  53. }
  54. return string(result), true
  55. }
  56. func absInt(x int) int {
  57. if x < 0 {
  58. return -x
  59. }
  60. return x
  61. }
  62. func GenerateTokenishPaddingBase62(targetHuffmanBytes int) string {
  63. n := int(math.Ceil(float64(targetHuffmanBytes) / avgHuffmanBytesPerCharBase62))
  64. if n < 1 {
  65. n = 1
  66. }
  67. randBase62Str, ok := randStringFromCharset(n, charsetBase62)
  68. if !ok {
  69. return ""
  70. }
  71. const maxIter = 150
  72. adjustChar := byte('X')
  73. // Adjust until close enough
  74. for iter := 0; iter < maxIter; iter++ {
  75. currentLength := int(hpack.HuffmanEncodeLength(randBase62Str))
  76. diff := currentLength - targetHuffmanBytes
  77. if absInt(diff) <= validationTolerance {
  78. return randBase62Str
  79. }
  80. if diff < 0 {
  81. // Too small -> append padding char(s)
  82. randBase62Str += string(adjustChar)
  83. // Avoid a long run of identical chars
  84. if adjustChar == 'X' {
  85. adjustChar = 'Z'
  86. } else {
  87. adjustChar = 'X'
  88. }
  89. } else {
  90. // Too big -> remove from the end
  91. if len(randBase62Str) <= 1 {
  92. return randBase62Str
  93. }
  94. randBase62Str = randBase62Str[:len(randBase62Str)-1]
  95. }
  96. }
  97. return randBase62Str
  98. }
  99. func GeneratePadding(method PaddingMethod, length int) string {
  100. if length <= 0 {
  101. return ""
  102. }
  103. // https://www.rfc-editor.org/rfc/rfc7541.html#appendix-B
  104. // h2's HPACK Header Compression feature employs a huffman encoding using a static table.
  105. // 'X' and 'Z' are assigned an 8 bit code, so HPACK compression won't change actual padding length on the wire.
  106. // https://www.rfc-editor.org/rfc/rfc9204.html#section-4.1.2-2
  107. // h3's similar QPACK feature uses the same huffman table.
  108. switch method {
  109. case PaddingMethodRepeatX:
  110. return strings.Repeat("X", length)
  111. case PaddingMethodTokenish:
  112. paddingValue := GenerateTokenishPaddingBase62(length)
  113. if paddingValue == "" {
  114. return strings.Repeat("X", length)
  115. }
  116. return paddingValue
  117. default:
  118. return strings.Repeat("X", length)
  119. }
  120. }
  121. func ApplyPaddingToCookie(req *http.Request, name, value string) {
  122. if req == nil || name == "" || value == "" {
  123. return
  124. }
  125. req.AddCookie(&http.Cookie{
  126. Name: name,
  127. Value: value,
  128. Path: "/",
  129. })
  130. }
  131. func ApplyPaddingToQuery(u *url.URL, key, value string) {
  132. if u == nil || key == "" || value == "" {
  133. return
  134. }
  135. q := u.Query()
  136. q.Set(key, value)
  137. u.RawQuery = q.Encode()
  138. }
  139. func (c *Config) GetNormalizedXPaddingBytes() RangeConfig {
  140. if c.XPaddingBytes == nil || c.XPaddingBytes.To == 0 {
  141. return RangeConfig{
  142. From: 100,
  143. To: 1000,
  144. }
  145. }
  146. return *c.XPaddingBytes
  147. }
  148. func (c *Config) ApplyXPaddingToHeader(h http.Header, config XPaddingConfig) {
  149. if h == nil {
  150. return
  151. }
  152. paddingValue := GeneratePadding(config.Method, config.Length)
  153. switch p := config.Placement; p.Placement {
  154. case PlacementHeader:
  155. h.Set(p.Header, paddingValue)
  156. case PlacementQueryInHeader:
  157. u, err := url.Parse(p.RawURL)
  158. if err != nil || u == nil {
  159. return
  160. }
  161. u.RawQuery = p.Key + "=" + paddingValue
  162. h.Set(p.Header, u.String())
  163. }
  164. }
  165. func (c *Config) ApplyXPaddingToRequest(req *http.Request, config XPaddingConfig) {
  166. if req == nil {
  167. return
  168. }
  169. if req.Header == nil {
  170. req.Header = make(http.Header)
  171. }
  172. placement := config.Placement.Placement
  173. if placement == PlacementHeader || placement == PlacementQueryInHeader {
  174. c.ApplyXPaddingToHeader(req.Header, config)
  175. return
  176. }
  177. paddingValue := GeneratePadding(config.Method, config.Length)
  178. switch placement {
  179. case PlacementCookie:
  180. ApplyPaddingToCookie(req, config.Placement.Key, paddingValue)
  181. case PlacementQuery:
  182. ApplyPaddingToQuery(req.URL, config.Placement.Key, paddingValue)
  183. }
  184. }
  185. func (c *Config) ExtractXPaddingFromRequest(req *http.Request, obfsMode bool) (string, string) {
  186. if req == nil {
  187. return "", ""
  188. }
  189. if !obfsMode {
  190. referrer := req.Header.Get("Referer")
  191. if referrer != "" {
  192. if referrerURL, err := url.Parse(referrer); err == nil {
  193. paddingValue := referrerURL.Query().Get("x_padding")
  194. paddingPlacement := PlacementQueryInHeader + "=Referer, key=x_padding"
  195. return paddingValue, paddingPlacement
  196. }
  197. } else {
  198. paddingValue := req.URL.Query().Get("x_padding")
  199. return paddingValue, PlacementQuery + ", key=x_padding"
  200. }
  201. }
  202. key := c.XPaddingKey
  203. header := c.XPaddingHeader
  204. if cookie, err := req.Cookie(key); err == nil {
  205. if cookie != nil && cookie.Value != "" {
  206. paddingValue := cookie.Value
  207. paddingPlacement := PlacementCookie + ", key=" + key
  208. return paddingValue, paddingPlacement
  209. }
  210. }
  211. headerValue := req.Header.Get(header)
  212. if headerValue != "" {
  213. if c.XPaddingPlacement == PlacementHeader {
  214. paddingPlacement := PlacementHeader + "=" + header
  215. return headerValue, paddingPlacement
  216. }
  217. if parsedURL, err := url.Parse(headerValue); err == nil {
  218. paddingPlacement := PlacementQueryInHeader + "=" + header + ", key=" + key
  219. return parsedURL.Query().Get(key), paddingPlacement
  220. }
  221. }
  222. queryValue := req.URL.Query().Get(key)
  223. if queryValue != "" {
  224. paddingPlacement := PlacementQuery + ", key=" + key
  225. return queryValue, paddingPlacement
  226. }
  227. return "", ""
  228. }
  229. func (c *Config) IsPaddingValid(paddingValue string, from, to int32, method PaddingMethod) bool {
  230. if paddingValue == "" {
  231. return false
  232. }
  233. if to <= 0 {
  234. r := c.GetNormalizedXPaddingBytes()
  235. from, to = r.From, r.To
  236. }
  237. switch method {
  238. case PaddingMethodRepeatX:
  239. n := int32(len(paddingValue))
  240. return n >= from && n <= to
  241. case PaddingMethodTokenish:
  242. const tolerance = int32(validationTolerance)
  243. n := int32(hpack.HuffmanEncodeLength(paddingValue))
  244. f := from - tolerance
  245. t := to + tolerance
  246. if f < 0 {
  247. f = 0
  248. }
  249. return n >= f && n <= t
  250. default:
  251. n := int32(len(paddingValue))
  252. return n >= from && n <= to
  253. }
  254. }