mirror of
https://github.com/MetaCubeX/mihomo.git
synced 2026-04-22 16:17:16 +08:00
feat: support xhttp new options for bypassing CDN's potential detection
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user