feat: support xhttp new options for bypassing CDN's potential detection

This commit is contained in:
wwqgtxx
2026-04-18 12:03:04 +08:00
parent 0e0265fdd6
commit 2337d70d86
10 changed files with 1048 additions and 299 deletions
+52
View File
@@ -85,6 +85,19 @@ type XHTTPOptions struct {
Headers map[string]string `proxy:"headers,omitempty"`
NoGRPCHeader bool `proxy:"no-grpc-header,omitempty"`
XPaddingBytes string `proxy:"x-padding-bytes,omitempty"`
XPaddingObfsMode bool `proxy:"x-padding-obfs-mode,omitempty"`
XPaddingKey string `proxy:"x-padding-key,omitempty"`
XPaddingHeader string `proxy:"x-padding-header,omitempty"`
XPaddingPlacement string `proxy:"x-padding-placement,omitempty"`
XPaddingMethod string `proxy:"x-padding-method,omitempty"`
UplinkHTTPMethod string `proxy:"uplink-http-method,omitempty"`
SessionPlacement string `proxy:"session-placement,omitempty"`
SessionKey string `proxy:"session-key,omitempty"`
SeqPlacement string `proxy:"seq-placement,omitempty"`
SeqKey string `proxy:"seq-key,omitempty"`
UplinkDataPlacement string `proxy:"uplink-data-placement,omitempty"`
UplinkDataKey string `proxy:"uplink-data-key,omitempty"`
UplinkChunkSize string `proxy:"uplink-chunk-size,omitempty"`
ScMaxEachPostBytes string `proxy:"sc-max-each-post-bytes,omitempty"`
ScMinPostsIntervalMs string `proxy:"sc-min-posts-interval-ms,omitempty"`
ReuseSettings *XHTTPReuseSettings `proxy:"reuse-settings,omitempty"` // aka XMUX
@@ -107,6 +120,19 @@ type XHTTPDownloadSettings struct {
Headers *map[string]string `proxy:"headers,omitempty"`
NoGRPCHeader *bool `proxy:"no-grpc-header,omitempty"`
XPaddingBytes *string `proxy:"x-padding-bytes,omitempty"`
XPaddingObfsMode *bool `proxy:"x-padding-obfs-mode,omitempty"`
XPaddingKey *string `proxy:"x-padding-key,omitempty"`
XPaddingHeader *string `proxy:"x-padding-header,omitempty"`
XPaddingPlacement *string `proxy:"x-padding-placement,omitempty"`
XPaddingMethod *string `proxy:"x-padding-method,omitempty"`
UplinkHTTPMethod *string `proxy:"uplink-http-method,omitempty"`
SessionPlacement *string `proxy:"session-placement,omitempty"`
SessionKey *string `proxy:"session-key,omitempty"`
SeqPlacement *string `proxy:"seq-placement,omitempty"`
SeqKey *string `proxy:"seq-key,omitempty"`
UplinkDataPlacement *string `proxy:"uplink-data-placement,omitempty"`
UplinkDataKey *string `proxy:"uplink-data-key,omitempty"`
UplinkChunkSize *string `proxy:"uplink-chunk-size,omitempty"`
ScMaxEachPostBytes *string `proxy:"sc-max-each-post-bytes,omitempty"`
ScMinPostsIntervalMs *string `proxy:"sc-min-posts-interval-ms,omitempty"`
ReuseSettings *XHTTPReuseSettings `proxy:"reuse-settings,omitempty"` // aka XMUX
@@ -552,6 +578,19 @@ func NewVless(option VlessOption) (*Vless, error) {
Headers: v.option.XHTTPOpts.Headers,
NoGRPCHeader: v.option.XHTTPOpts.NoGRPCHeader,
XPaddingBytes: v.option.XHTTPOpts.XPaddingBytes,
XPaddingObfsMode: v.option.XHTTPOpts.XPaddingObfsMode,
XPaddingKey: v.option.XHTTPOpts.XPaddingKey,
XPaddingHeader: v.option.XHTTPOpts.XPaddingHeader,
XPaddingPlacement: v.option.XHTTPOpts.XPaddingPlacement,
XPaddingMethod: v.option.XHTTPOpts.XPaddingMethod,
UplinkHTTPMethod: v.option.XHTTPOpts.UplinkHTTPMethod,
SessionPlacement: v.option.XHTTPOpts.SessionPlacement,
SessionKey: v.option.XHTTPOpts.SessionKey,
SeqPlacement: v.option.XHTTPOpts.SeqPlacement,
SeqKey: v.option.XHTTPOpts.SeqKey,
UplinkDataPlacement: v.option.XHTTPOpts.UplinkDataPlacement,
UplinkDataKey: v.option.XHTTPOpts.UplinkDataKey,
UplinkChunkSize: v.option.XHTTPOpts.UplinkChunkSize,
ScMaxEachPostBytes: v.option.XHTTPOpts.ScMaxEachPostBytes,
ScMinPostsIntervalMs: v.option.XHTTPOpts.ScMinPostsIntervalMs,
ReuseConfig: reuseCfg,
@@ -667,6 +706,19 @@ func NewVless(option VlessOption) (*Vless, error) {
Headers: lo.FromPtrOr(ds.Headers, v.option.XHTTPOpts.Headers),
NoGRPCHeader: lo.FromPtrOr(ds.NoGRPCHeader, v.option.XHTTPOpts.NoGRPCHeader),
XPaddingBytes: lo.FromPtrOr(ds.XPaddingBytes, v.option.XHTTPOpts.XPaddingBytes),
XPaddingObfsMode: lo.FromPtrOr(ds.XPaddingObfsMode, v.option.XHTTPOpts.XPaddingObfsMode),
XPaddingKey: lo.FromPtrOr(ds.XPaddingKey, v.option.XHTTPOpts.XPaddingKey),
XPaddingHeader: lo.FromPtrOr(ds.XPaddingHeader, v.option.XHTTPOpts.XPaddingHeader),
XPaddingPlacement: lo.FromPtrOr(ds.XPaddingPlacement, v.option.XHTTPOpts.XPaddingPlacement),
XPaddingMethod: lo.FromPtrOr(ds.XPaddingMethod, v.option.XHTTPOpts.XPaddingMethod),
UplinkHTTPMethod: lo.FromPtrOr(ds.UplinkHTTPMethod, v.option.XHTTPOpts.UplinkHTTPMethod),
SessionPlacement: lo.FromPtrOr(ds.SessionPlacement, v.option.XHTTPOpts.SessionPlacement),
SessionKey: lo.FromPtrOr(ds.SessionKey, v.option.XHTTPOpts.SessionKey),
SeqPlacement: lo.FromPtrOr(ds.SeqPlacement, v.option.XHTTPOpts.SeqPlacement),
SeqKey: lo.FromPtrOr(ds.SeqKey, v.option.XHTTPOpts.SeqKey),
UplinkDataPlacement: lo.FromPtrOr(ds.UplinkDataPlacement, v.option.XHTTPOpts.UplinkDataPlacement),
UplinkDataKey: lo.FromPtrOr(ds.UplinkDataKey, v.option.XHTTPOpts.UplinkDataKey),
UplinkChunkSize: lo.FromPtrOr(ds.UplinkChunkSize, v.option.XHTTPOpts.UplinkChunkSize),
ScMaxEachPostBytes: lo.FromPtrOr(ds.ScMaxEachPostBytes, v.option.XHTTPOpts.ScMaxEachPostBytes),
ScMinPostsIntervalMs: lo.FromPtrOr(ds.ScMinPostsIntervalMs, v.option.XHTTPOpts.ScMinPostsIntervalMs),
ReuseConfig: downloadReuseCfg,
+40
View File
@@ -825,6 +825,19 @@ proxies: # socks5
# X-Forwarded-For: ""
# no-grpc-header: false
# x-padding-bytes: "100-1000"
# x-padding-obfs-mode: false
# x-padding-key: x_padding
# x-padding-header: Referer
# x-padding-placement: queryInHeader # Available: queryInHeader, cookie, header, query
# x-padding-method: repeat-x # Available: repeat-x, tokenish
# uplink-http-method: POST # Available: POST, PUT, PATCH, DELETE
# session-placement: path # Available: path, query, cookie, header
# session-key: ""
# seq-placement: path # Available: path, query, cookie, header
# seq-key: ""
# uplink-data-placement: body # Available: body, cookie, header
# uplink-data-key: ""
# uplink-chunk-size: 0 # only applicable when uplink-data-placement is not body
# sc-max-each-post-bytes: 1000000
# sc-min-posts-interval-ms: 30
# reuse-settings: # aka XMUX
@@ -842,6 +855,19 @@ proxies: # socks5
# X-Forwarded-For: ""
# no-grpc-header: false
# x-padding-bytes: "100-1000"
# x-padding-obfs-mode: false
# x-padding-key: x_padding
# x-padding-header: Referer
# x-padding-placement: queryInHeader # Available: queryInHeader, cookie, header, query
# x-padding-method: repeat-x # Available: repeat-x, tokenish
# uplink-http-method: POST # Available: POST, PUT, PATCH, DELETE
# session-placement: path # Available: path, query, cookie, header
# session-key: ""
# seq-placement: path # Available: path, query, cookie, header
# seq-key: ""
# uplink-data-placement: body # Available: body, cookie, header
# uplink-data-key: ""
# uplink-chunk-size: 0 # only applicable when uplink-data-placement is not body
# sc-max-each-post-bytes: 1000000
# sc-min-posts-interval-ms: 30
# reuse-settings: # aka XMUX
@@ -1687,6 +1713,20 @@ listeners:
# host: ""
# mode: auto # Available: "stream-one", "stream-up" or "packet-up"
# no-sse-header: false
# x-padding-bytes: "100-1000"
# x-padding-obfs-mode: false
# x-padding-key: x_padding
# x-padding-header: Referer
# x-padding-placement: queryInHeader # Available: queryInHeader, cookie, header, query
# x-padding-method: repeat-x # Available: repeat-x, tokenish
# uplink-http-method: POST # Available: POST, PUT, PATCH, DELETE
# session-placement: path # Available: path, query, cookie, header
# session-key: ""
# seq-placement: path # Available: path, query, cookie, header
# seq-key: ""
# uplink-data-placement: body # Available: body, cookie, header
# uplink-data-key: ""
# uplink-chunk-size: 0 # only applicable when uplink-data-placement is not body
# sc-max-buffered-posts: 30
# sc-stream-up-server-secs: "20-80"
# sc-max-each-post-bytes: 1000000
+14
View File
@@ -34,6 +34,20 @@ type XHTTPConfig struct {
Path string
Host string
Mode string
XPaddingBytes string
XPaddingObfsMode bool
XPaddingKey string
XPaddingHeader string
XPaddingPlacement string
XPaddingMethod string
UplinkHTTPMethod string
SessionPlacement string
SessionKey string
SeqPlacement string
SeqKey string
UplinkDataPlacement string
UplinkDataKey string
UplinkChunkSize string
NoSSEHeader bool
ScStreamUpServerSecs string
ScMaxBufferedPosts string
+27
View File
@@ -35,6 +35,20 @@ type XHTTPConfig struct {
Path string `inbound:"path,omitempty"`
Host string `inbound:"host,omitempty"`
Mode string `inbound:"mode,omitempty"`
XPaddingBytes string `inbound:"xpadding-bytes,omitempty"`
XPaddingObfsMode bool `inbound:"xpadding-obfs-mode,omitempty"`
XPaddingKey string `inbound:"xpadding-key,omitempty"`
XPaddingHeader string `inbound:"xpadding-header,omitempty"`
XPaddingPlacement string `inbound:"xpadding-placement,omitempty"`
XPaddingMethod string `inbound:"xpadding-method,omitempty"`
UplinkHTTPMethod string `inbound:"uplink-http-method,omitempty"`
SessionPlacement string `inbound:"session-placement,omitempty"`
SessionKey string `inbound:"session-key,omitempty"`
SeqPlacement string `inbound:"seq-placement,omitempty"`
SeqKey string `inbound:"seq-key,omitempty"`
UplinkDataPlacement string `inbound:"uplink-data-placement,omitempty"`
UplinkDataKey string `inbound:"uplink-data-key,omitempty"`
UplinkChunkSize string `inbound:"uplink-chunk-size,omitempty"`
NoSSEHeader bool `inbound:"no-sse-header,omitempty"`
ScStreamUpServerSecs string `inbound:"sc-stream-up-server-secs,omitempty"`
ScMaxBufferedPosts string `inbound:"sc-max-buffered-posts,omitempty"`
@@ -47,6 +61,19 @@ func (o XHTTPConfig) Build() LC.XHTTPConfig {
Host: o.Host,
Mode: o.Mode,
NoSSEHeader: o.NoSSEHeader,
XPaddingBytes: o.XPaddingBytes,
XPaddingObfsMode: o.XPaddingObfsMode,
XPaddingKey: o.XPaddingKey,
XPaddingHeader: o.XPaddingHeader,
XPaddingPlacement: o.XPaddingPlacement,
UplinkHTTPMethod: o.UplinkHTTPMethod,
SessionPlacement: o.SessionPlacement,
SessionKey: o.SessionKey,
SeqPlacement: o.SeqPlacement,
SeqKey: o.SeqKey,
UplinkDataPlacement: o.UplinkDataPlacement,
UplinkDataKey: o.UplinkDataKey,
UplinkChunkSize: o.UplinkChunkSize,
ScStreamUpServerSecs: o.ScStreamUpServerSecs,
ScMaxBufferedPosts: o.ScMaxBufferedPosts,
ScMaxEachPostBytes: o.ScMaxEachPostBytes,
+14
View File
@@ -159,6 +159,20 @@ func New(config LC.VlessServer, tunnel C.Tunnel, additions ...inbound.Addition)
Host: config.XHTTPConfig.Host,
Path: config.XHTTPConfig.Path,
Mode: config.XHTTPConfig.Mode,
XPaddingBytes: config.XHTTPConfig.XPaddingBytes,
XPaddingObfsMode: config.XHTTPConfig.XPaddingObfsMode,
XPaddingKey: config.XHTTPConfig.XPaddingKey,
XPaddingHeader: config.XHTTPConfig.XPaddingHeader,
XPaddingPlacement: config.XHTTPConfig.XPaddingPlacement,
XPaddingMethod: config.XHTTPConfig.XPaddingMethod,
UplinkHTTPMethod: config.XHTTPConfig.UplinkHTTPMethod,
SessionPlacement: config.XHTTPConfig.SessionPlacement,
SessionKey: config.XHTTPConfig.SessionKey,
SeqPlacement: config.XHTTPConfig.SeqPlacement,
SeqKey: config.XHTTPConfig.SeqKey,
UplinkDataPlacement: config.XHTTPConfig.UplinkDataPlacement,
UplinkDataKey: config.XHTTPConfig.UplinkDataKey,
UplinkChunkSize: config.XHTTPConfig.UplinkChunkSize,
NoSSEHeader: config.XHTTPConfig.NoSSEHeader,
ScStreamUpServerSecs: config.XHTTPConfig.ScStreamUpServerSecs,
ScMaxBufferedPosts: config.XHTTPConfig.ScMaxBufferedPosts,
+3 -3
View File
@@ -115,7 +115,7 @@ func (c *PacketUpWriter) write(b []byte) (int, error) {
Path: c.cfg.NormalizedPath(),
}
req, err := http.NewRequestWithContext(c.ctx, http.MethodPost, u.String(), nil)
req, err := http.NewRequestWithContext(c.ctx, c.cfg.GetNormalizedUplinkHTTPMethod(), u.String(), nil)
if err != nil {
return 0, err
}
@@ -359,7 +359,7 @@ func (c *Client) DialStreamOne() (net.Conn, error) {
},
})
req, err := http.NewRequestWithContext(ctx, http.MethodPost, requestURL.String(), pr)
req, err := http.NewRequestWithContext(ctx, c.cfg.GetNormalizedUplinkHTTPMethod(), requestURL.String(), pr)
if err != nil {
_ = pr.Close()
_ = pw.Close()
@@ -470,7 +470,7 @@ func (c *Client) DialStreamUp() (net.Conn, error) {
uploadReq, err := http.NewRequestWithContext(
c.ctx,
http.MethodPost,
c.cfg.GetNormalizedUplinkHTTPMethod(),
streamURL.String(),
pr,
)
+336 -57
View File
@@ -2,6 +2,7 @@ package xhttp
import (
"bytes"
"encoding/base64"
"fmt"
"io"
"math/rand"
@@ -11,6 +12,16 @@ import (
"github.com/metacubex/http"
)
const (
PlacementQueryInHeader = "queryInHeader"
PlacementCookie = "cookie"
PlacementHeader = "header"
PlacementQuery = "query"
PlacementPath = "path"
PlacementBody = "body"
PlacementAuto = "auto"
)
type Config struct {
Host string
Path string
@@ -18,6 +29,19 @@ type Config struct {
Headers map[string]string
NoGRPCHeader bool
XPaddingBytes string
XPaddingObfsMode bool
XPaddingKey string
XPaddingHeader string
XPaddingPlacement string
XPaddingMethod string
UplinkHTTPMethod string
SessionPlacement string
SessionKey string
SeqPlacement string
SeqKey string
UplinkDataPlacement string
UplinkDataKey string
UplinkChunkSize string
NoSSEHeader bool // server only
ScStreamUpServerSecs string // server only
ScMaxBufferedPosts string // server only
@@ -70,7 +94,7 @@ func (c *Config) NormalizedPath() string {
return path
}
func (c *Config) RequestHeader() http.Header {
func (c *Config) GetRequestHeader() http.Header {
h := http.Header{}
for k, v := range c.Headers {
h.Set(k, v)
@@ -95,12 +119,83 @@ func (c *Config) RequestHeader() http.Header {
return h
}
func (c *Config) RandomPadding() (string, error) {
r, err := ParseRange(c.XPaddingBytes, "100-1000")
if err != nil {
return "", fmt.Errorf("invalid x-padding-bytes: %w", err)
func (c *Config) GetRequestHeaderWithPayload(payload []byte, uplinkChunkSize Range) http.Header {
header := c.GetRequestHeader()
key := c.UplinkDataKey
encodedData := base64.RawURLEncoding.EncodeToString(payload)
for i := 0; len(encodedData) > 0; i++ {
chunkSize := uplinkChunkSize.Rand()
if len(encodedData) < chunkSize {
chunkSize = len(encodedData)
}
chunk := encodedData[:chunkSize]
encodedData = encodedData[chunkSize:]
headerKey := fmt.Sprintf("%s-%d", key, i)
header.Set(headerKey, chunk)
}
return strings.Repeat("X", r.Rand()), nil
return header
}
func (c *Config) GetRequestCookiesWithPayload(payload []byte, uplinkChunkSize Range) []*http.Cookie {
cookies := []*http.Cookie{}
key := c.UplinkDataKey
encodedData := base64.RawURLEncoding.EncodeToString(payload)
for i := 0; len(encodedData) > 0; i++ {
chunkSize := uplinkChunkSize.Rand()
if len(encodedData) < chunkSize {
chunkSize = len(encodedData)
}
chunk := encodedData[:chunkSize]
encodedData = encodedData[chunkSize:]
cookieName := fmt.Sprintf("%s_%d", key, i)
cookies = append(cookies, &http.Cookie{Name: cookieName, Value: chunk})
}
return cookies
}
func (c *Config) WriteResponseHeader(writer http.ResponseWriter, requestMethod string, requestHeader http.Header) {
if origin := requestHeader.Get("Origin"); origin == "" {
writer.Header().Set("Access-Control-Allow-Origin", "*")
} else {
// Chrome says: The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'.
writer.Header().Set("Access-Control-Allow-Origin", origin)
}
if c.GetNormalizedSessionPlacement() == PlacementCookie ||
c.GetNormalizedSeqPlacement() == PlacementCookie ||
c.XPaddingPlacement == PlacementCookie ||
c.GetNormalizedUplinkDataPlacement() == PlacementCookie {
writer.Header().Set("Access-Control-Allow-Credentials", "true")
}
if requestMethod == "OPTIONS" {
requestedMethod := requestHeader.Get("Access-Control-Request-Method")
if requestedMethod != "" {
writer.Header().Set("Access-Control-Allow-Methods", requestedMethod)
} else {
writer.Header().Set("Access-Control-Allow-Methods", "*")
}
requestedHeaders := requestHeader.Get("Access-Control-Request-Headers")
if requestedHeaders == "" {
writer.Header().Set("Access-Control-Allow-Headers", "*")
} else {
writer.Header().Set("Access-Control-Allow-Headers", requestedHeaders)
}
}
}
func (c *Config) GetNormalizedUplinkHTTPMethod() string {
if c.UplinkHTTPMethod == "" {
return "POST"
}
return c.UplinkHTTPMethod
}
func (c *Config) GetNormalizedScStreamUpServerSecs() (Range, error) {
@@ -144,6 +239,84 @@ func (c *Config) GetNormalizedScMinPostsIntervalMs() (Range, error) {
return r, nil
}
func (c *Config) GetNormalizedUplinkChunkSize() (Range, error) {
uplinkChunkSize, err := ParseRange(c.UplinkChunkSize, "")
if err != nil {
return Range{}, fmt.Errorf("invalid uplink-chunk-size: %w", err)
}
if uplinkChunkSize.Max == 0 {
switch c.GetNormalizedUplinkDataPlacement() {
case PlacementCookie:
return Range{
Min: 2 * 1024, // 2 KiB
Max: 3 * 1024, // 3 KiB
}, nil
case PlacementHeader:
return Range{
Min: 3 * 1024, // 3 KiB
Max: 4 * 1024, // 4 KiB
}, nil
default:
return c.GetNormalizedScMaxEachPostBytes()
}
} else if uplinkChunkSize.Min < 64 {
uplinkChunkSize.Min = 64
if uplinkChunkSize.Max < 64 {
uplinkChunkSize.Max = 64
}
}
return uplinkChunkSize, nil
}
func (c *Config) GetNormalizedSessionPlacement() string {
if c.SessionPlacement == "" {
return PlacementPath
}
return c.SessionPlacement
}
func (c *Config) GetNormalizedSeqPlacement() string {
if c.SeqPlacement == "" {
return PlacementPath
}
return c.SeqPlacement
}
func (c *Config) GetNormalizedUplinkDataPlacement() string {
if c.UplinkDataPlacement == "" {
return PlacementBody
}
return c.UplinkDataPlacement
}
func (c *Config) GetNormalizedSessionKey() string {
if c.SessionKey != "" {
return c.SessionKey
}
switch c.GetNormalizedSessionPlacement() {
case PlacementHeader:
return "X-Session"
case PlacementCookie, PlacementQuery:
return "x_session"
default:
return ""
}
}
func (c *Config) GetNormalizedSeqKey() string {
if c.SeqKey != "" {
return c.SeqKey
}
switch c.GetNormalizedSeqPlacement() {
case PlacementHeader:
return "X-Seq"
case PlacementCookie, PlacementQuery:
return "x_seq"
default:
return ""
}
}
type Range struct {
Min int
Max int
@@ -231,32 +404,6 @@ func (c *ReuseConfig) ResolveEntryConfig() (Range, Range, Range, error) {
return cMaxReuseTimes, hMaxRequestTimes, hMaxReusableSecs, nil
}
func (c *Config) FillStreamRequest(req *http.Request, sessionID string) error {
req.Header = c.RequestHeader()
paddingValue, err := c.RandomPadding()
if err != nil {
return err
}
if paddingValue != "" {
rawURL := req.URL.String()
sep := "?"
if strings.Contains(rawURL, "?") {
sep = "&"
}
req.Header.Set("Referer", rawURL+sep+"x_padding="+paddingValue)
}
c.ApplyMetaToRequest(req, sessionID, "")
if req.Body != nil && !c.NoGRPCHeader {
req.Header.Set("Content-Type", "application/grpc")
}
return nil
}
func appendToPath(path, value string) string {
if strings.HasSuffix(path, "/") {
return path + value
@@ -264,53 +411,185 @@ func appendToPath(path, value string) string {
return path + "/" + value
}
func (c *Config) ApplyMetaToRequest(req *http.Request, sessionID string, seqStr string) {
if sessionID != "" {
req.URL.Path = appendToPath(req.URL.Path, sessionID)
func (c *Config) ApplyMetaToRequest(req *http.Request, sessionId string, seqStr string) {
sessionPlacement := c.GetNormalizedSessionPlacement()
seqPlacement := c.GetNormalizedSeqPlacement()
sessionKey := c.GetNormalizedSessionKey()
seqKey := c.GetNormalizedSeqKey()
if sessionId != "" {
switch sessionPlacement {
case PlacementPath:
req.URL.Path = appendToPath(req.URL.Path, sessionId)
case PlacementQuery:
q := req.URL.Query()
q.Set(sessionKey, sessionId)
req.URL.RawQuery = q.Encode()
case PlacementHeader:
req.Header.Set(sessionKey, sessionId)
case PlacementCookie:
req.AddCookie(&http.Cookie{Name: sessionKey, Value: sessionId})
}
}
if seqStr != "" {
req.URL.Path = appendToPath(req.URL.Path, seqStr)
switch seqPlacement {
case PlacementPath:
req.URL.Path = appendToPath(req.URL.Path, seqStr)
case PlacementQuery:
q := req.URL.Query()
q.Set(seqKey, seqStr)
req.URL.RawQuery = q.Encode()
case PlacementHeader:
req.Header.Set(seqKey, seqStr)
case PlacementCookie:
req.AddCookie(&http.Cookie{Name: seqKey, Value: seqStr})
}
}
}
func (c *Config) FillPacketRequest(req *http.Request, sessionID string, seqStr string, payload []byte) error {
req.Header = c.RequestHeader()
req.Body = io.NopCloser(bytes.NewReader(payload))
req.ContentLength = int64(len(payload))
func (c *Config) ExtractMetaFromRequest(req *http.Request, path string) (sessionId string, seqStr string) {
sessionPlacement := c.GetNormalizedSessionPlacement()
seqPlacement := c.GetNormalizedSeqPlacement()
sessionKey := c.GetNormalizedSessionKey()
seqKey := c.GetNormalizedSeqKey()
paddingValue, err := c.RandomPadding()
var subpath []string
pathPart := 0
if sessionPlacement == PlacementPath || seqPlacement == PlacementPath {
subpath = strings.Split(req.URL.Path[len(path):], "/")
}
switch sessionPlacement {
case PlacementPath:
if len(subpath) > pathPart {
sessionId = subpath[pathPart]
pathPart += 1
}
case PlacementQuery:
sessionId = req.URL.Query().Get(sessionKey)
case PlacementHeader:
sessionId = req.Header.Get(sessionKey)
case PlacementCookie:
if cookie, e := req.Cookie(sessionKey); e == nil {
sessionId = cookie.Value
}
}
switch seqPlacement {
case PlacementPath:
if len(subpath) > pathPart {
seqStr = subpath[pathPart]
pathPart += 1
}
case PlacementQuery:
seqStr = req.URL.Query().Get(seqKey)
case PlacementHeader:
seqStr = req.Header.Get(seqKey)
case PlacementCookie:
if cookie, e := req.Cookie(seqKey); e == nil {
seqStr = cookie.Value
}
}
return sessionId, seqStr
}
func (c *Config) FillStreamRequest(req *http.Request, sessionID string) error {
req.Header = c.GetRequestHeader()
xPaddingBytes, err := c.GetNormalizedXPaddingBytes()
if err != nil {
return err
}
if paddingValue != "" {
rawURL := req.URL.String()
sep := "?"
if strings.Contains(rawURL, "?") {
sep = "&"
length := xPaddingBytes.Rand()
config := XPaddingConfig{Length: length}
if c.XPaddingObfsMode {
config.Placement = XPaddingPlacement{
Placement: c.XPaddingPlacement,
Key: c.XPaddingKey,
Header: c.XPaddingHeader,
RawURL: req.URL.String(),
}
config.Method = PaddingMethod(c.XPaddingMethod)
} else {
config.Placement = XPaddingPlacement{
Placement: PlacementQueryInHeader,
Key: "x_padding",
Header: "Referer",
RawURL: req.URL.String(),
}
req.Header.Set("Referer", rawURL+sep+"x_padding="+paddingValue)
}
c.ApplyMetaToRequest(req, sessionID, seqStr)
c.ApplyXPaddingToRequest(req, config)
c.ApplyMetaToRequest(req, sessionID, "")
if req.Body != nil && !c.NoGRPCHeader { // stream-up/one
req.Header.Set("Content-Type", "application/grpc")
}
return nil
}
func (c *Config) FillDownloadRequest(req *http.Request, sessionID string) error {
req.Header = c.RequestHeader()
return c.FillStreamRequest(req, sessionID)
}
paddingValue, err := c.RandomPadding()
func (c *Config) FillPacketRequest(request *http.Request, sessionId string, seqStr string, data []byte) error {
dataPlacement := c.GetNormalizedUplinkDataPlacement()
if dataPlacement == PlacementBody || dataPlacement == PlacementAuto {
request.Header = c.GetRequestHeader()
request.Body = io.NopCloser(bytes.NewReader(data))
request.ContentLength = int64(len(data))
} else {
request.Body = nil
request.ContentLength = 0
switch dataPlacement {
case PlacementHeader:
uplinkChunkSize, err := c.GetNormalizedUplinkChunkSize()
if err != nil {
return err
}
request.Header = c.GetRequestHeaderWithPayload(data, uplinkChunkSize)
case PlacementCookie:
request.Header = c.GetRequestHeader()
uplinkChunkSize, err := c.GetNormalizedUplinkChunkSize()
if err != nil {
return err
}
for _, cookie := range c.GetRequestCookiesWithPayload(data, uplinkChunkSize) {
request.AddCookie(cookie)
}
}
}
xPaddingBytes, err := c.GetNormalizedXPaddingBytes()
if err != nil {
return err
}
if paddingValue != "" {
rawURL := req.URL.String()
sep := "?"
if strings.Contains(rawURL, "?") {
sep = "&"
length := xPaddingBytes.Rand()
config := XPaddingConfig{Length: length}
if c.XPaddingObfsMode {
config.Placement = XPaddingPlacement{
Placement: c.XPaddingPlacement,
Key: c.XPaddingKey,
Header: c.XPaddingHeader,
RawURL: request.URL.String(),
}
config.Method = PaddingMethod(c.XPaddingMethod)
} else {
config.Placement = XPaddingPlacement{
Placement: PlacementQueryInHeader,
Key: "x_padding",
Header: "Referer",
RawURL: request.URL.String(),
}
req.Header.Set("Referer", rawURL+sep+"x_padding="+paddingValue)
}
c.ApplyMetaToRequest(req, sessionID, "")
c.ApplyXPaddingToRequest(request, config)
c.ApplyMetaToRequest(request, sessionId, seqStr)
return nil
}
+229 -137
View File
@@ -1,6 +1,9 @@
package xhttp
import (
"bytes"
"encoding/base64"
"fmt"
"io"
"net"
"strconv"
@@ -98,6 +101,7 @@ type requestHandler struct {
connHandler func(net.Conn)
httpHandler http.Handler
xPaddingBytes Range
scMaxEachPostBytes Range
scStreamUpServerSecs Range
scMaxBufferedPosts Range
@@ -107,6 +111,10 @@ type requestHandler struct {
}
func NewServerHandler(opt ServerOption) (http.Handler, error) {
xPaddingBytes, err := opt.Config.GetNormalizedXPaddingBytes()
if err != nil {
return nil, err
}
scMaxEachPostBytes, err := opt.Config.GetNormalizedScMaxEachPostBytes()
if err != nil {
return nil, err
@@ -125,6 +133,7 @@ func NewServerHandler(opt ServerOption) (http.Handler, error) {
config: opt.Config,
connHandler: opt.ConnHandler,
httpHandler: opt.HttpHandler,
xPaddingBytes: xPaddingBytes,
scMaxEachPostBytes: scMaxEachPostBytes,
scStreamUpServerSecs: scStreamUpServerSecs,
scMaxBufferedPosts: scMaxBufferedPosts,
@@ -134,7 +143,7 @@ func NewServerHandler(opt ServerOption) (http.Handler, error) {
}), nil
}
func (h *requestHandler) getOrCreateSession(sessionID string) *httpSession {
func (h *requestHandler) upsertSession(sessionID string) *httpSession {
h.mu.Lock()
defer h.mu.Unlock()
@@ -161,8 +170,6 @@ func (h *requestHandler) getOrCreateSession(sessionID string) *httpSession {
return s
}
func (h *requestHandler) deleteSession(sessionID string) {
h.mu.Lock()
defer h.mu.Unlock()
@@ -239,11 +246,227 @@ func (h *requestHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}
rest := strings.TrimPrefix(r.URL.Path, path)
parts := splitNonEmpty(rest)
h.config.WriteResponseHeader(w, r.Method, r.Header)
length := h.xPaddingBytes.Rand()
config := XPaddingConfig{Length: length}
if h.config.XPaddingObfsMode {
config.Placement = XPaddingPlacement{
Placement: h.config.XPaddingPlacement,
Key: h.config.XPaddingKey,
Header: h.config.XPaddingHeader,
}
config.Method = PaddingMethod(h.config.XPaddingMethod)
} else {
config.Placement = XPaddingPlacement{
Placement: PlacementHeader,
Header: "X-Padding",
}
}
h.config.ApplyXPaddingToResponse(w, config)
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
paddingValue, _ := h.config.ExtractXPaddingFromRequest(r, h.config.XPaddingObfsMode)
if !h.config.IsPaddingValid(paddingValue, h.xPaddingBytes.Min, h.xPaddingBytes.Max, PaddingMethod(h.config.XPaddingMethod)) {
http.Error(w, "invalid xpadding", http.StatusBadRequest)
return
}
sessionId, seqStr := h.config.ExtractMetaFromRequest(r, path)
var currentSession *httpSession
if sessionId != "" {
currentSession = h.upsertSession(sessionId)
}
// stream-up upload: POST /path/{session}
if r.Method != http.MethodGet && sessionId != "" && seqStr == "" && h.allowStreamUpUpload() {
httpSC := newHTTPServerConn(w, r.Body)
err := currentSession.uploadQueue.Push(Packet{
Reader: httpSC,
})
if err != nil {
http.Error(w, err.Error(), http.StatusConflict)
return
}
// magic header instructs nginx + apache to not buffer response body
w.Header().Set("X-Accel-Buffering", "no")
// A web-compliant header telling all middleboxes to disable caching.
// Should be able to prevent overloading the cache, or stop CDNs from
// teeing the response stream into their cache, causing slowdowns.
w.Header().Set("Cache-Control", "no-store")
if !h.config.NoSSEHeader {
// magic header to make the HTTP middle box consider this as SSE to disable buffer
w.Header().Set("Content-Type", "text/event-stream")
}
w.WriteHeader(http.StatusOK)
referrer := r.Header.Get("Referer")
if referrer != "" && h.scStreamUpServerSecs.Max > 0 {
go func() {
for {
_, err := httpSC.Write(bytes.Repeat([]byte{'X'}, int(h.xPaddingBytes.Rand())))
if err != nil {
break
}
time.Sleep(time.Duration(h.scStreamUpServerSecs.Rand()) * time.Second)
}
}()
}
select {
case <-r.Context().Done():
case <-httpSC.Wait():
}
_ = httpSC.Close()
return
}
// packet-up upload: POST /path/{session}/{seq}
if r.Method != http.MethodGet && sessionId != "" && seqStr != "" && h.allowPacketUpUpload() {
scMaxEachPostBytes := h.scMaxEachPostBytes.Max
dataPlacement := h.config.GetNormalizedUplinkDataPlacement()
uplinkDataKey := h.config.UplinkDataKey
var headerPayload []byte
var err error
if dataPlacement == PlacementAuto || dataPlacement == PlacementHeader {
var headerPayloadChunks []string
for i := 0; true; i++ {
chunk := r.Header.Get(fmt.Sprintf("%s-%d", uplinkDataKey, i))
if chunk == "" {
break
}
headerPayloadChunks = append(headerPayloadChunks, chunk)
}
headerPayloadEncoded := strings.Join(headerPayloadChunks, "")
headerPayload, err = base64.RawURLEncoding.DecodeString(headerPayloadEncoded)
if err != nil {
http.Error(w, "invalid base64 in header's payload", http.StatusBadRequest)
return
}
}
var cookiePayload []byte
if dataPlacement == PlacementAuto || dataPlacement == PlacementCookie {
var cookiePayloadChunks []string
for i := 0; true; i++ {
cookieName := fmt.Sprintf("%s_%d", uplinkDataKey, i)
if c, _ := r.Cookie(cookieName); c != nil {
cookiePayloadChunks = append(cookiePayloadChunks, c.Value)
} else {
break
}
}
cookiePayloadEncoded := strings.Join(cookiePayloadChunks, "")
cookiePayload, err = base64.RawURLEncoding.DecodeString(cookiePayloadEncoded)
if err != nil {
http.Error(w, "invalid base64 in cookies' payload", http.StatusBadRequest)
return
}
}
var bodyPayload []byte
if dataPlacement == PlacementAuto || dataPlacement == PlacementBody {
if r.ContentLength > int64(scMaxEachPostBytes) {
http.Error(w, "body too large", http.StatusRequestEntityTooLarge)
return
}
bodyPayload, err = io.ReadAll(io.LimitReader(r.Body, int64(scMaxEachPostBytes)+1))
if err != nil {
http.Error(w, "failed to read body", http.StatusBadRequest)
return
}
}
var payload []byte
switch dataPlacement {
case PlacementHeader:
payload = headerPayload
case PlacementCookie:
payload = cookiePayload
case PlacementBody:
payload = bodyPayload
case PlacementAuto:
payload = headerPayload
payload = append(payload, cookiePayload...)
payload = append(payload, bodyPayload...)
}
if len(payload) > h.scMaxEachPostBytes.Max {
http.Error(w, "body too large", http.StatusRequestEntityTooLarge)
return
}
seq, err := strconv.ParseUint(seqStr, 10, 64)
if err != nil {
http.Error(w, "invalid xhttp seq", http.StatusBadRequest)
return
}
err = currentSession.uploadQueue.Push(Packet{
Seq: seq,
Payload: payload,
})
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if len(payload) == 0 {
// Methods without a body are usually cached by default.
w.Header().Set("Cache-Control", "no-store")
}
w.WriteHeader(http.StatusOK)
return
}
// stream-up/packet-up download: GET /path/{session}
if r.Method == http.MethodGet && sessionId != "" && seqStr == "" && h.allowSessionDownload() {
currentSession.markConnected()
// magic header instructs nginx + apache to not buffer response body
w.Header().Set("X-Accel-Buffering", "no")
// A web-compliant header telling all middleboxes to disable caching.
// Should be able to prevent overloading the cache, or stop CDNs from
// teeing the response stream into their cache, causing slowdowns.
w.Header().Set("Cache-Control", "no-store")
if !h.config.NoSSEHeader {
// magic header to make the HTTP middle box consider this as SSE to disable buffer
w.Header().Set("Content-Type", "text/event-stream")
}
w.WriteHeader(http.StatusOK)
if flusher, ok := w.(http.Flusher); ok {
flusher.Flush()
}
httpSC := newHTTPServerConn(w, r.Body)
conn := &Conn{
writer: httpSC,
reader: currentSession.uploadQueue,
onClose: func() {
h.deleteSession(sessionId)
},
}
httputils.SetAddrFromRequest(&conn.NetAddr, r)
go h.connHandler(N.NewDeadlineConn(conn))
select {
case <-r.Context().Done():
case <-httpSC.Wait():
}
_ = conn.Close()
return
}
// stream-one: POST /path
if r.Method == http.MethodPost && len(parts) == 0 && h.allowStreamOne() {
if r.Method != http.MethodGet && sessionId == "" && seqStr == "" && h.allowStreamOne() {
w.Header().Set("X-Accel-Buffering", "no")
w.Header().Set("Cache-Control", "no-store")
w.WriteHeader(http.StatusOK)
@@ -269,137 +492,6 @@ func (h *requestHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}
// stream-up/packet-up download: GET /path/{session}
if r.Method == http.MethodGet && len(parts) == 1 && h.allowSessionDownload() {
sessionID := parts[0]
session := h.getOrCreateSession(sessionID)
session.markConnected()
// magic header instructs nginx + apache to not buffer response body
w.Header().Set("X-Accel-Buffering", "no")
// A web-compliant header telling all middleboxes to disable caching.
// Should be able to prevent overloading the cache, or stop CDNs from
// teeing the response stream into their cache, causing slowdowns.
w.Header().Set("Cache-Control", "no-store")
if !h.config.NoSSEHeader {
// magic header to make the HTTP middle box consider this as SSE to disable buffer
w.Header().Set("Content-Type", "text/event-stream")
}
w.WriteHeader(http.StatusOK)
if flusher, ok := w.(http.Flusher); ok {
flusher.Flush()
}
httpSC := newHTTPServerConn(w, r.Body)
conn := &Conn{
writer: httpSC,
reader: session.uploadQueue,
onClose: func() {
h.deleteSession(sessionID)
},
}
httputils.SetAddrFromRequest(&conn.NetAddr, r)
go h.connHandler(N.NewDeadlineConn(conn))
select {
case <-r.Context().Done():
case <-httpSC.Wait():
}
_ = conn.Close()
return
}
// stream-up upload: POST /path/{session}
if r.Method == http.MethodPost && len(parts) == 1 && h.allowStreamUpUpload() {
sessionID := parts[0]
session := h.getOrCreateSession(sessionID)
httpSC := newHTTPServerConn(w, r.Body)
err := session.uploadQueue.Push(Packet{
Reader: httpSC,
})
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// magic header instructs nginx + apache to not buffer response body
w.Header().Set("X-Accel-Buffering", "no")
// A web-compliant header telling all middleboxes to disable caching.
// Should be able to prevent overloading the cache, or stop CDNs from
// teeing the response stream into their cache, causing slowdowns.
w.Header().Set("Cache-Control", "no-store")
if !h.config.NoSSEHeader {
// magic header to make the HTTP middle box consider this as SSE to disable buffer
w.Header().Set("Content-Type", "text/event-stream")
}
w.WriteHeader(http.StatusOK)
referrer := r.Header.Get("Referer")
if referrer != "" && h.scStreamUpServerSecs.Max > 0 {
go func() {
for {
paddingValue, _ := h.config.RandomPadding()
if paddingValue == "" {
break
}
_, err = httpSC.Write([]byte(paddingValue))
if err != nil {
break
}
time.Sleep(time.Duration(h.scStreamUpServerSecs.Rand()) * time.Second)
}
}()
}
select {
case <-r.Context().Done():
case <-httpSC.Wait():
}
_ = httpSC.Close()
return
}
// packet-up upload: POST /path/{session}/{seq}
if r.Method == http.MethodPost && len(parts) == 2 && h.allowPacketUpUpload() {
sessionID := parts[0]
seq, err := strconv.ParseUint(parts[1], 10, 64)
if err != nil {
http.Error(w, "invalid xhttp seq", http.StatusBadRequest)
return
}
session := h.getOrCreateSession(sessionID)
if r.ContentLength > int64(h.scMaxEachPostBytes.Max) {
http.Error(w, "body too large", http.StatusRequestEntityTooLarge)
return
}
body, err := io.ReadAll(io.LimitReader(r.Body, int64(h.scMaxEachPostBytes.Max)+1))
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
err = session.uploadQueue.Push(Packet{
Seq: seq,
Payload: body,
})
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if len(body) == 0 {
w.Header().Set("Cache-Control", "no-store")
}
w.WriteHeader(http.StatusOK)
return
}
http.NotFound(w, r)
}
-102
View File
@@ -1,102 +0,0 @@
package xhttp
import (
"io"
"net"
"testing"
"github.com/metacubex/http"
"github.com/metacubex/http/httptest"
"github.com/stretchr/testify/assert"
)
func TestServerHandlerModeRestrictions(t *testing.T) {
testCases := []struct {
name string
mode string
method string
target string
wantStatus int
}{
{
name: "StreamOneAcceptsStreamOne",
mode: "stream-one",
method: http.MethodPost,
target: "https://example.com/xhttp/",
wantStatus: http.StatusOK,
},
{
name: "StreamOneRejectsSessionDownload",
mode: "stream-one",
method: http.MethodGet,
target: "https://example.com/xhttp/session",
wantStatus: http.StatusNotFound,
},
{
name: "StreamUpAcceptsStreamOne",
mode: "stream-up",
method: http.MethodPost,
target: "https://example.com/xhttp/",
wantStatus: http.StatusOK,
},
{
name: "StreamUpAllowsDownloadEndpoint",
mode: "stream-up",
method: http.MethodGet,
target: "https://example.com/xhttp/session",
wantStatus: http.StatusOK,
},
{
name: "StreamUpRejectsPacketUpload",
mode: "stream-up",
method: http.MethodPost,
target: "https://example.com/xhttp/session/0",
wantStatus: http.StatusNotFound,
},
{
name: "PacketUpAllowsDownloadEndpoint",
mode: "packet-up",
method: http.MethodGet,
target: "https://example.com/xhttp/session",
wantStatus: http.StatusOK,
},
{
name: "PacketUpRejectsStreamOne",
mode: "packet-up",
method: http.MethodPost,
target: "https://example.com/xhttp/",
wantStatus: http.StatusNotFound,
},
{
name: "PacketUpRejectsStreamUpUpload",
mode: "packet-up",
method: http.MethodPost,
target: "https://example.com/xhttp/session",
wantStatus: http.StatusNotFound,
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
handler, err := NewServerHandler(ServerOption{
Config: Config{
Path: "/xhttp",
Mode: testCase.mode,
},
ConnHandler: func(conn net.Conn) {
_ = conn.Close()
},
})
if err != nil {
panic(err)
}
req := httptest.NewRequest(testCase.method, testCase.target, io.NopCloser(http.NoBody))
recorder := httptest.NewRecorder()
handler.ServeHTTP(recorder, req)
assert.Equal(t, testCase.wantStatus, recorder.Result().StatusCode)
})
}
}
+333
View File
@@ -0,0 +1,333 @@
package xhttp
import (
"crypto/rand"
"fmt"
"math"
"net/url"
"strings"
"github.com/metacubex/http"
"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 ApplyPaddingToResponseCookie(writer http.ResponseWriter, name, value string) {
if name == "" || value == "" {
return
}
http.SetCookie(writer, &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() (Range, error) {
r, err := ParseRange(c.XPaddingBytes, "100-1000")
if err != nil {
return Range{}, fmt.Errorf("invalid x-padding-bytes: %w", err)
}
return r, nil
}
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) ApplyXPaddingToResponse(writer http.ResponseWriter, config XPaddingConfig) {
placement := config.Placement.Placement
if placement == PlacementHeader || placement == PlacementQueryInHeader {
c.ApplyXPaddingToHeader(writer.Header(), config)
return
}
paddingValue := GeneratePadding(config.Method, config.Length)
switch placement {
case PlacementCookie:
ApplyPaddingToResponseCookie(writer, 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 int, method PaddingMethod) bool {
if paddingValue == "" {
return false
}
if to <= 0 {
if r, err := c.GetNormalizedXPaddingBytes(); err == nil {
from, to = r.Min, r.Max
}
}
switch method {
case PaddingMethodRepeatX:
n := len(paddingValue)
return n >= from && n <= to
case PaddingMethodTokenish:
const tolerance = validationTolerance
n := int(hpack.HuffmanEncodeLength(paddingValue))
f := from - tolerance
t := to + tolerance
if f < 0 {
f = 0
}
return n >= f && n <= t
default:
n := len(paddingValue)
return n >= from && n <= to
}
}