Update xhttp

This commit is contained in:
Shtorm
2026-02-22 14:48:52 +03:00
parent 9e4eb52a82
commit a3aaf2cb92
9 changed files with 863 additions and 104 deletions

View File

@@ -3,7 +3,6 @@ package pipe
import ( import (
"errors" "errors"
"io" "io"
"runtime"
"sync" "sync"
"time" "time"
@@ -136,11 +135,10 @@ func (p *pipe) writeMultiBufferInternal(mb buf.MultiBuffer) error {
if p.data == nil { if p.data == nil {
p.data = mb p.data = mb
return nil } else {
p.data, _ = buf.MergeMulti(p.data, mb)
} }
return nil
p.data, _ = buf.MergeMulti(p.data, mb)
return errSlowDown
} }
func (p *pipe) WriteMultiBuffer(mb buf.MultiBuffer) error { func (p *pipe) WriteMultiBuffer(mb buf.MultiBuffer) error {
@@ -155,30 +153,23 @@ func (p *pipe) WriteMultiBuffer(mb buf.MultiBuffer) error {
return nil return nil
} }
if err == errSlowDown { if err == errBufferFull {
p.readSignal.Signal() if p.option.discardOverflow {
buf.ReleaseMulti(mb)
// Yield current goroutine. Hopefully the reading counterpart can pick up the payload. return nil
runtime.Gosched() }
return nil select {
case <-p.writeSignal.Wait():
continue
case <-p.done.Wait():
buf.ReleaseMulti(mb)
return io.ErrClosedPipe
}
} }
if err == errBufferFull && p.option.discardOverflow { buf.ReleaseMulti(mb)
buf.ReleaseMulti(mb) p.readSignal.Signal()
return nil return err
}
if err != errBufferFull {
buf.ReleaseMulti(mb)
p.readSignal.Signal()
return err
}
select {
case <-p.writeSignal.Wait():
case <-p.done.Wait():
return io.ErrClosedPipe
}
} }
} }

View File

@@ -0,0 +1,28 @@
package utils
import (
"math/rand"
"strconv"
"time"
"github.com/klauspost/cpuid/v2"
)
func ChromeVersion() int {
// Use only CPU info as seed for PRNG
seed := int64(cpuid.CPU.Family + cpuid.CPU.Model + cpuid.CPU.PhysicalCores + cpuid.CPU.LogicalCores + cpuid.CPU.CacheLine)
rng := rand.New(rand.NewSource(seed))
// Start from Chrome 144 released on 2026.1.13
releaseDate := time.Date(2026, 1, 13, 0, 0, 0, 0, time.UTC)
version := 144
now := time.Now()
// Each version has random 25-45 day interval
for releaseDate.Before(now) {
releaseDate = releaseDate.AddDate(0, 0, rng.Intn(21)+25)
version++
}
return version - 1
}
// ChromeUA provides default browser User-Agent based on CPU-seeded PRNG.
var ChromeUA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/" + strconv.Itoa(ChromeVersion()) + ".0.0.0 Safari/537.36"

View File

@@ -0,0 +1,24 @@
package utils
import (
"math/rand/v2"
)
var (
// 8 ÷ (397/62)
h2packCorrectionFactor = 1.2493702770780857
base62TotalCharsNum = 62
base62Chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
)
// H2Base62Pad generates a base62 padding string for HTTP/2 header
// The total len will be slightly longer than the input to match the length after h2(h3 also) header huffman encoding
func H2Base62Pad[T int32 | int64 | int](expectedLen T) string {
actualLenFloat := float64(expectedLen) * h2packCorrectionFactor
actualLen := int(actualLenFloat)
result := make([]byte, actualLen)
for i := range actualLen {
result[i] = base62Chars[rand.N(base62TotalCharsNum)]
}
return string(result)
}

View File

@@ -85,10 +85,14 @@ func ParseString(str string) (UUID, error) {
b := uuid.Bytes() b := uuid.Bytes()
for _, byteGroup := range byteGroups { for _, byteGroup := range byteGroups {
if text[0] == '-' { if len(text) > 0 && text[0] == '-' {
text = text[1:] text = text[1:]
} }
if len(text) < byteGroup {
return uuid, E.New("invalid UUID: ", str)
}
if _, err := hex.Decode(b[:byteGroup/2], text[:byteGroup]); err != nil { if _, err := hex.Decode(b[:byteGroup/2], text[:byteGroup]); err != nil {
return uuid, err return uuid, err
} }

View File

@@ -2,10 +2,10 @@ package option
import ( import (
"net/http" "net/http"
"net/url"
"strings" "strings"
Xbadoption "github.com/sagernet/sing-box/common/xray/json/badoption" Xbadoption "github.com/sagernet/sing-box/common/xray/json/badoption"
"github.com/sagernet/sing-box/common/xray/utils"
C "github.com/sagernet/sing-box/constant" C "github.com/sagernet/sing-box/constant"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json" "github.com/sagernet/sing/common/json"
@@ -122,14 +122,29 @@ type V2RayXHTTPBaseOptions struct {
ScMaxBufferedPosts int64 `json:"sc_max_buffered_posts,omitempty"` ScMaxBufferedPosts int64 `json:"sc_max_buffered_posts,omitempty"`
ScStreamUpServerSecs Xbadoption.Range `json:"sc_stream_up_server_secs"` ScStreamUpServerSecs Xbadoption.Range `json:"sc_stream_up_server_secs"`
Xmux *V2RayXHTTPXmuxOptions `json:"xmux"` Xmux *V2RayXHTTPXmuxOptions `json:"xmux"`
XPaddingObfsMode bool `json:"x_padding_obfs_mode,omitempty"`
XPaddingKey string `json:"x_padding_key,omitempty"`
XPaddingHeader string `json:"x_padding_header,omitempty"`
XPaddingPlacement string `json:"x_padding_placement,omitempty"`
XPaddingMethod string `json:"x_padding_method,omitempty"`
UplinkHTTPMethod string `json:"uplink_http_method,omitempty"`
SessionPlacement string `json:"session_placement,omitempty"`
SessionKey string `json:"session_key,omitempty"`
SeqPlacement string `json:"seq_placement,omitempty"`
SeqKey string `json:"seq_key,omitempty"`
UplinkDataPlacement string `json:"uplink_data_placement,omitempty"`
UplinkDataKey string `json:"uplink_data_key,omitempty"`
UplinkChunkSize uint32 `json:"uplink_chunk_size,omitempty"`
} }
type V2RayXHTTPOptions struct { type _V2RayXHTTPOptions struct {
Mode string `json:"mode"` Mode string `json:"mode"`
V2RayXHTTPBaseOptions V2RayXHTTPBaseOptions
Download *V2RayXHTTPDownloadOptions `json:"download"` Download *V2RayXHTTPDownloadOptions `json:"download"`
} }
type V2RayXHTTPOptions _V2RayXHTTPOptions
type V2RayXHTTPDownloadOptions struct { type V2RayXHTTPDownloadOptions struct {
V2RayXHTTPBaseOptions V2RayXHTTPBaseOptions
ServerOptions ServerOptions
@@ -137,6 +152,153 @@ type V2RayXHTTPDownloadOptions struct {
Detour string `json:"detour,omitempty"` Detour string `json:"detour,omitempty"`
} }
const (
PlacementQueryInHeader = "queryInHeader"
PlacementCookie = "cookie"
PlacementHeader = "header"
PlacementQuery = "query"
PlacementPath = "path"
PlacementBody = "body"
)
func (c *V2RayXHTTPOptions) UnmarshalJSON(bytes []byte) error {
err := json.Unmarshal(bytes, (*_V2RayXHTTPOptions)(c))
if err != nil {
return err
}
switch c.Mode {
case "":
c.Mode = "auto"
case "auto", "packet-up", "stream-up", "stream-one":
default:
return E.New("unsupported mode: " + c.Mode)
}
err = checkV2RayXHTTPBaseOptions(c.Mode, &c.V2RayXHTTPBaseOptions)
if err != nil {
return err
}
if c.Download != nil {
err = checkV2RayXHTTPBaseOptions(c.Mode, &c.Download.V2RayXHTTPBaseOptions)
if err != nil {
return err
}
}
return nil
}
func checkV2RayXHTTPBaseOptions(mode string, options *V2RayXHTTPBaseOptions) error {
// Priority (client): host > serverName > address
for k := range options.Headers {
if strings.ToLower(k) == "host" {
return E.New(`"headers" can't contain "host"`)
}
}
if options.XPaddingBytes.From <= 0 || options.XPaddingBytes.To <= 0 {
return E.New("xPaddingBytes cannot be disabled")
}
if options.XPaddingKey == "" {
options.XPaddingKey = "x_padding"
}
if options.XPaddingHeader == "" {
options.XPaddingHeader = "X-Padding"
}
switch options.XPaddingPlacement {
case "":
options.XPaddingPlacement = "queryInHeader"
case "cookie", "header", "query", "queryInHeader":
default:
return E.New("unsupported padding placement: " + options.XPaddingPlacement)
}
switch options.XPaddingMethod {
case "":
options.XPaddingMethod = "repeat-x"
case "repeat-x", "tokenish":
default:
return E.New("unsupported padding method: " + options.XPaddingMethod)
}
switch options.UplinkDataPlacement {
case "":
options.UplinkDataPlacement = "body"
case "body":
case "cookie", "header":
if mode != "packet-up" {
return E.New("UplinkDataPlacement can be " + options.UplinkDataPlacement + " only in packet-up mode")
}
default:
return E.New("unsupported uplink data placement: " + options.UplinkDataPlacement)
}
if options.UplinkHTTPMethod == "" {
options.UplinkHTTPMethod = "POST"
}
options.UplinkHTTPMethod = strings.ToUpper(options.UplinkHTTPMethod)
if options.UplinkHTTPMethod == "GET" && mode != "packet-up" {
return E.New("uplinkHTTPMethod can be GET only in packet-up mode")
}
switch options.SessionPlacement {
case "":
options.SessionPlacement = "path"
case "path", "cookie", "header", "query":
default:
return E.New("unsupported session placement: " + options.SessionPlacement)
}
switch options.SeqPlacement {
case "":
options.SeqPlacement = "path"
case "path", "cookie", "header", "query":
if options.SessionPlacement == "path" {
return E.New("SeqPlacement must be path when SessionPlacement is path")
}
default:
return E.New("unsupported seq placement: " + options.SeqPlacement)
}
if options.SessionPlacement != "path" && options.SessionKey == "" {
switch options.SessionPlacement {
case "cookie", "query":
options.SessionKey = "x_session"
case "header":
options.SessionKey = "X-Session"
}
}
if options.SeqPlacement != "path" && options.SeqKey == "" {
switch options.SeqPlacement {
case "cookie", "query":
options.SeqKey = "x_seq"
case "header":
options.SeqKey = "X-Seq"
}
}
if options.UplinkDataPlacement != "body" && options.UplinkDataKey == "" {
switch options.UplinkDataPlacement {
case "cookie":
options.UplinkDataKey = "x_data"
case "header":
options.UplinkDataKey = "X-Data"
}
}
if options.UplinkChunkSize == 0 {
switch options.UplinkDataPlacement {
case "cookie":
options.UplinkChunkSize = 3 * 1024 // 3KB
case "header":
options.UplinkChunkSize = 4 * 1024 // 4KB
}
} else if options.UplinkChunkSize < 64 {
options.UplinkChunkSize = 64
}
if options.Xmux == nil {
options.Xmux = &V2RayXHTTPXmuxOptions{}
options.Xmux.MaxConcurrency.From = 1
options.Xmux.MaxConcurrency.To = 1
options.Xmux.HMaxRequestTimes.From = 600
options.Xmux.HMaxRequestTimes.To = 900
options.Xmux.HMaxReusableSecs.From = 1800
options.Xmux.HMaxReusableSecs.To = 3000
} else if options.Xmux.MaxConnections.To > 0 && options.Xmux.MaxConcurrency.To > 0 {
return E.New("maxConnections cannot be specified together with maxConcurrency")
}
return nil
}
func (c *V2RayXHTTPBaseOptions) GetNormalizedPath() string { func (c *V2RayXHTTPBaseOptions) GetNormalizedPath() string {
pathAndQuery := strings.SplitN(c.Path, "?", 2) pathAndQuery := strings.SplitN(c.Path, "?", 2)
path := pathAndQuery[0] path := pathAndQuery[0]
@@ -158,19 +320,14 @@ func (c *V2RayXHTTPBaseOptions) GetNormalizedQuery() string {
return query return query
} }
func (c *V2RayXHTTPBaseOptions) GetRequestHeader(rawURL string) http.Header { func (c *V2RayXHTTPBaseOptions) GetRequestHeader() http.Header {
header := http.Header{} header := http.Header{}
for k, v := range c.Headers { for k, v := range c.Headers {
header.Add(k, v) header.Add(k, v)
} }
u, _ := url.Parse(rawURL) if header.Get("User-Agent") == "" {
// https://www.rfc-editor.org/rfc/rfc7541.html#appendix-B header.Set("User-Agent", utils.ChromeUA)
// h2's HPACK Header Compression feature employs a huffman encoding using a static table. }
// 'X' is 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.
u.RawQuery = "x_padding=" + strings.Repeat("X", int(c.GetNormalizedXPaddingBytes().Rand()))
header.Set("Referer", u.String())
return header return header
} }
@@ -184,6 +341,13 @@ func (c *V2RayXHTTPBaseOptions) GetNormalizedXPaddingBytes() Xbadoption.Range {
return c.XPaddingBytes return c.XPaddingBytes
} }
func (c *V2RayXHTTPBaseOptions) GetNormalizedUplinkHTTPMethod() string {
if c.UplinkHTTPMethod == "" {
return "POST"
}
return c.UplinkHTTPMethod
}
func (c *V2RayXHTTPBaseOptions) GetNormalizedScMaxEachPostBytes() Xbadoption.Range { func (c *V2RayXHTTPBaseOptions) GetNormalizedScMaxEachPostBytes() Xbadoption.Range {
if c.ScMaxEachPostBytes.To == 0 { if c.ScMaxEachPostBytes.To == 0 {
return Xbadoption.Range{ return Xbadoption.Range{
@@ -222,6 +386,55 @@ func (c *V2RayXHTTPBaseOptions) GetNormalizedScStreamUpServerSecs() Xbadoption.R
return c.ScStreamUpServerSecs return c.ScStreamUpServerSecs
} }
func (c *V2RayXHTTPBaseOptions) GetNormalizedSessionPlacement() string {
if c.SessionPlacement == "" {
return PlacementPath
}
return c.SessionPlacement
}
func (c *V2RayXHTTPBaseOptions) GetNormalizedSeqPlacement() string {
if c.SeqPlacement == "" {
return PlacementPath
}
return c.SeqPlacement
}
func (c *V2RayXHTTPBaseOptions) GetNormalizedUplinkDataPlacement() string {
if c.UplinkDataPlacement == "" {
return PlacementBody
}
return c.UplinkDataPlacement
}
func (c *V2RayXHTTPBaseOptions) 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 *V2RayXHTTPBaseOptions) 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 V2RayXHTTPXmuxOptions struct { type V2RayXHTTPXmuxOptions struct {
MaxConcurrency Xbadoption.Range `json:"max_concurrency"` MaxConcurrency Xbadoption.Range `json:"max_concurrency"`
MaxConnections Xbadoption.Range `json:"max_connections"` MaxConnections Xbadoption.Range `json:"max_connections"`

View File

@@ -35,12 +35,12 @@ import (
) )
type Client struct { type Client struct {
ctx context.Context ctx context.Context
options *option.V2RayXHTTPOptions options *option.V2RayXHTTPOptions
getRequestURL func(sessionId string) url.URL baseRequestURL url.URL
getRequestURL2 func(sessionId string) url.URL baseRequestURL2 url.URL
getHTTPClient func() (DialerClient, *XmuxClient) getHTTPClient func() (DialerClient, *XmuxClient)
getHTTPClient2 func() (DialerClient, *XmuxClient) getHTTPClient2 func() (DialerClient, *XmuxClient)
} }
func NewClient(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, options option.V2RayXHTTPOptions, tlsConfig tls.Config) (adapter.V2RayClientTransport, error) { func NewClient(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, options option.V2RayXHTTPOptions, tlsConfig tls.Config) (adapter.V2RayClientTransport, error) {
@@ -48,17 +48,10 @@ func NewClient(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, opt
return nil, E.New("mode is not set") return nil, E.New("mode is not set")
} }
dest := serverAddr dest := serverAddr
baseRequestURL, err := getBaseRequestURL( baseRequestURL, err := getBaseRequestURL(&options.V2RayXHTTPBaseOptions, dest, tlsConfig)
&options.V2RayXHTTPBaseOptions, dest, tlsConfig,
)
if err != nil { if err != nil {
return nil, err return nil, err
} }
getRequestURL := func(sessionId string) url.URL {
requestURL := baseRequestURL
requestURL.Path += sessionId
return requestURL
}
var xmuxOptions option.V2RayXHTTPXmuxOptions var xmuxOptions option.V2RayXHTTPXmuxOptions
if options.Xmux != nil { if options.Xmux != nil {
xmuxOptions = *options.Xmux xmuxOptions = *options.Xmux
@@ -70,7 +63,7 @@ func NewClient(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, opt
xmuxClient := xmuxManager.GetXmuxClient(ctx) xmuxClient := xmuxManager.GetXmuxClient(ctx)
return xmuxClient.XmuxConn.(DialerClient), xmuxClient return xmuxClient.XmuxConn.(DialerClient), xmuxClient
} }
getRequestURL2 := getRequestURL baseRequestURL2 := baseRequestURL
getHTTPClient2 := getHTTPClient getHTTPClient2 := getHTTPClient
if options.Download != nil { if options.Download != nil {
options2 := options.Download options2 := options.Download
@@ -90,15 +83,10 @@ func NewClient(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, opt
return nil, err return nil, err
} }
} }
baseRequestURL2, err := getBaseRequestURL(&options2.V2RayXHTTPBaseOptions, dest2, tlsConfig2) baseRequestURL2, err = getBaseRequestURL(&options2.V2RayXHTTPBaseOptions, dest2, tlsConfig2)
if err != nil { if err != nil {
return nil, err return nil, err
} }
getRequestURL2 = func(sessionId string) url.URL {
requestURL2 := baseRequestURL2
requestURL2.Path += sessionId
return requestURL2
}
var xmuxOptions2 option.V2RayXHTTPXmuxOptions var xmuxOptions2 option.V2RayXHTTPXmuxOptions
if options2.Xmux != nil { if options2.Xmux != nil {
xmuxOptions2 = *options2.Xmux xmuxOptions2 = *options2.Xmux
@@ -112,21 +100,25 @@ func NewClient(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, opt
} }
} }
return &Client{ return &Client{
ctx: ctx, ctx: ctx,
options: &options, options: &options,
getHTTPClient: getHTTPClient, getHTTPClient: getHTTPClient,
getHTTPClient2: getHTTPClient2, getHTTPClient2: getHTTPClient2,
getRequestURL: getRequestURL, baseRequestURL: baseRequestURL,
getRequestURL2: getRequestURL2, baseRequestURL2: baseRequestURL2,
}, nil }, nil
} }
func (c *Client) DialContext(ctx context.Context) (net.Conn, error) { func (c *Client) DialContext(ctx context.Context) (net.Conn, error) {
options := c.options options := c.options
mode := c.options.Mode mode := c.options.Mode
sessionIdUuid := uuid.New() sessionId := ""
requestURL := c.getRequestURL(sessionIdUuid.String()) if c.options.Mode != "stream-one" {
requestURL2 := c.getRequestURL2(sessionIdUuid.String()) sessionIdUuid := uuid.New()
sessionId = sessionIdUuid.String()
}
requestURL := c.baseRequestURL
requestURL2 := c.baseRequestURL2
httpClient, xmuxClient := c.getHTTPClient() httpClient, xmuxClient := c.getHTTPClient()
httpClient2, xmuxClient2 := c.getHTTPClient2() httpClient2, xmuxClient2 := c.getHTTPClient2()
if xmuxClient != nil { if xmuxClient != nil {
@@ -157,7 +149,7 @@ func (c *Client) DialContext(ctx context.Context) (net.Conn, error) {
if xmuxClient != nil { if xmuxClient != nil {
xmuxClient.LeftRequests.Add(-1) xmuxClient.LeftRequests.Add(-1)
} }
conn.reader, conn.remoteAddr, conn.localAddr, err = httpClient.OpenStream(ctx, requestURL.String(), reader, false) conn.reader, conn.remoteAddr, conn.localAddr, err = httpClient.OpenStream(ctx, requestURL.String(), sessionId, reader, false)
if err != nil { // browser dialer only if err != nil { // browser dialer only
return nil, err return nil, err
} }
@@ -166,7 +158,7 @@ func (c *Client) DialContext(ctx context.Context) (net.Conn, error) {
if xmuxClient2 != nil { if xmuxClient2 != nil {
xmuxClient2.LeftRequests.Add(-1) xmuxClient2.LeftRequests.Add(-1)
} }
conn.reader, conn.remoteAddr, conn.localAddr, err = httpClient2.OpenStream(ctx, requestURL2.String(), nil, false) conn.reader, conn.remoteAddr, conn.localAddr, err = httpClient2.OpenStream(ctx, requestURL2.String(), sessionId, nil, false)
if err != nil { // browser dialer only if err != nil { // browser dialer only
return nil, err return nil, err
} }
@@ -175,7 +167,7 @@ func (c *Client) DialContext(ctx context.Context) (net.Conn, error) {
if xmuxClient != nil { if xmuxClient != nil {
xmuxClient.LeftRequests.Add(-1) xmuxClient.LeftRequests.Add(-1)
} }
_, _, _, err = httpClient.OpenStream(ctx, requestURL.String(), reader, true) _, _, _, err = httpClient.OpenStream(ctx, requestURL.String(), sessionId, reader, true)
if err != nil { // browser dialer only if err != nil { // browser dialer only
return nil, err return nil, err
} }
@@ -209,7 +201,7 @@ func (c *Client) DialContext(ctx context.Context) (net.Conn, error) {
// this intentionally makes a shallow-copy of the struct so we // this intentionally makes a shallow-copy of the struct so we
// can reassign Path (potentially concurrently) // can reassign Path (potentially concurrently)
url := requestURL url := requestURL
url.Path += "/" + strconv.FormatInt(seq, 10) seqStr := strconv.FormatInt(seq, 10)
seq += 1 seq += 1
if scMinPostsIntervalMs.From > 0 { if scMinPostsIntervalMs.From > 0 {
time.Sleep(time.Duration(scMinPostsIntervalMs.Rand())*time.Millisecond - time.Since(lastWrite)) time.Sleep(time.Duration(scMinPostsIntervalMs.Rand())*time.Millisecond - time.Since(lastWrite))
@@ -230,6 +222,8 @@ func (c *Client) DialContext(ctx context.Context) (net.Conn, error) {
err := httpClient.PostPacket( err := httpClient.PostPacket(
ctx, ctx,
url.String(), url.String(),
sessionId,
seqStr,
&buf.MultiBufferContainer{MultiBuffer: chunk}, &buf.MultiBufferContainer{MultiBuffer: chunk},
int64(chunk.Len()), int64(chunk.Len()),
) )

View File

@@ -3,14 +3,16 @@ package xhttp
import ( import (
"bytes" "bytes"
"context" "context"
"encoding/base64"
"fmt" "fmt"
"io" "io"
"net" "net"
"net/http" "net/http"
"net/http/httptrace" "net/http/httptrace"
"strings"
"sync" "sync"
"github.com/sagernet/sing-box/common/xray" common "github.com/sagernet/sing-box/common/xray"
"github.com/sagernet/sing-box/common/xray/signal/done" "github.com/sagernet/sing-box/common/xray/signal/done"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
) )
@@ -19,11 +21,11 @@ import (
type DialerClient interface { type DialerClient interface {
IsClosed() bool IsClosed() bool
// ctx, url, body, uploadOnly // ctx, url, sessionId, body, uploadOnly
OpenStream(context.Context, string, io.Reader, bool) (io.ReadCloser, net.Addr, net.Addr, error) OpenStream(context.Context, string, string, io.Reader, bool) (io.ReadCloser, net.Addr, net.Addr, error)
// ctx, url, body, contentLength // ctx, url, sessionId, seqStr, body, contentLength
PostPacket(context.Context, string, io.Reader, int64) error PostPacket(context.Context, string, string, string, io.Reader, int64) error
} }
// implements xhttp.DialerClient in terms of direct network connections // implements xhttp.DialerClient in terms of direct network connections
@@ -41,7 +43,7 @@ func (c *DefaultDialerClient) IsClosed() bool {
return c.closed return c.closed
} }
func (c *DefaultDialerClient) OpenStream(ctx context.Context, url string, body io.Reader, uploadOnly bool) (wrc io.ReadCloser, remoteAddr, localAddr net.Addr, err error) { func (c *DefaultDialerClient) OpenStream(ctx context.Context, url string, sessionId string, body io.Reader, uploadOnly bool) (wrc io.ReadCloser, remoteAddr, localAddr net.Addr, err error) {
// this is done when the TCP/UDP connection to the server was established, // this is done when the TCP/UDP connection to the server was established,
// and we can unblock the Dial function and print correct net addresses in // and we can unblock the Dial function and print correct net addresses in
// logs // logs
@@ -55,11 +57,31 @@ func (c *DefaultDialerClient) OpenStream(ctx context.Context, url string, body i
}) })
method := "GET" // stream-down method := "GET" // stream-down
if body != nil { if body != nil {
method = "POST" // stream-up/one method = c.options.GetNormalizedUplinkHTTPMethod() // stream-up/one
} }
req, _ := http.NewRequestWithContext(context.WithoutCancel(ctx), method, url, body) req, _ := http.NewRequestWithContext(context.WithoutCancel(ctx), method, url, body)
req.Header = c.options.GetRequestHeader(url) req.Header = c.options.GetRequestHeader()
if method == "POST" && !c.options.NoGRPCHeader { length := int(c.options.GetNormalizedXPaddingBytes().Rand())
config := XPaddingConfig{Length: length}
if c.options.XPaddingObfsMode {
config.Placement = XPaddingPlacement{
Placement: c.options.XPaddingPlacement,
Key: c.options.XPaddingKey,
Header: c.options.XPaddingHeader,
RawURL: url,
}
config.Method = PaddingMethod(c.options.XPaddingMethod)
} else {
config.Placement = XPaddingPlacement{
Placement: option.PlacementQueryInHeader,
Key: "x_padding",
Header: "Referer",
RawURL: url,
}
}
ApplyXPaddingToRequest(req, config)
ApplyMetaToRequest(c.options, req, sessionId, "")
if method == c.options.GetNormalizedUplinkHTTPMethod() && !c.options.NoGRPCHeader {
req.Header.Set("Content-Type", "application/grpc") req.Header.Set("Content-Type", "application/grpc")
} }
wrc = &WaitReadCloser{Wait: make(chan struct{})} wrc = &WaitReadCloser{Wait: make(chan struct{})}
@@ -85,13 +107,76 @@ func (c *DefaultDialerClient) OpenStream(ctx context.Context, url string, body i
return return
} }
func (c *DefaultDialerClient) PostPacket(ctx context.Context, url string, body io.Reader, contentLength int64) error { func (c *DefaultDialerClient) PostPacket(ctx context.Context, url string, sessionId string, seqStr string, body io.Reader, contentLength int64) error {
req, err := http.NewRequestWithContext(context.WithoutCancel(ctx), "POST", url, body) var encodedData string
dataPlacement := c.options.GetNormalizedUplinkDataPlacement()
if dataPlacement != option.PlacementBody {
data, err := io.ReadAll(body)
if err != nil {
return err
}
encodedData = base64.RawURLEncoding.EncodeToString(data)
body = nil
contentLength = 0
}
method := c.options.GetNormalizedUplinkHTTPMethod()
req, err := http.NewRequestWithContext(context.WithoutCancel(ctx), method, url, body)
if err != nil { if err != nil {
return err return err
} }
req.ContentLength = contentLength req.ContentLength = contentLength
req.Header = c.options.GetRequestHeader(url) req.Header = c.options.GetRequestHeader()
if dataPlacement != option.PlacementBody {
key := c.options.UplinkDataKey
chunkSize := int(c.options.UplinkChunkSize)
switch dataPlacement {
case option.PlacementHeader:
for i := 0; i < len(encodedData); i += chunkSize {
end := i + chunkSize
if end > len(encodedData) {
end = len(encodedData)
}
chunk := encodedData[i:end]
headerKey := fmt.Sprintf("%s-%d", key, i/chunkSize)
req.Header.Set(headerKey, chunk)
}
req.Header.Set(key+"-Length", fmt.Sprintf("%d", len(encodedData)))
req.Header.Set(key+"-Upstream", "1")
case option.PlacementCookie:
for i := 0; i < len(encodedData); i += chunkSize {
end := i + chunkSize
if end > len(encodedData) {
end = len(encodedData)
}
chunk := encodedData[i:end]
cookieName := fmt.Sprintf("%s_%d", key, i/chunkSize)
req.AddCookie(&http.Cookie{Name: cookieName, Value: chunk})
}
req.AddCookie(&http.Cookie{Name: key + "_upstream", Value: "1"})
}
}
length := int(c.options.GetNormalizedXPaddingBytes().Rand())
config := XPaddingConfig{Length: length}
if c.options.XPaddingObfsMode {
config.Placement = XPaddingPlacement{
Placement: c.options.XPaddingPlacement,
Key: c.options.XPaddingKey,
Header: c.options.XPaddingHeader,
RawURL: url,
}
config.Method = PaddingMethod(c.options.XPaddingMethod)
} else {
config.Placement = XPaddingPlacement{
Placement: option.PlacementQueryInHeader,
Key: "x_padding",
Header: "Referer",
RawURL: url,
}
}
ApplyXPaddingToRequest(req, config)
ApplyMetaToRequest(c.options, req, sessionId, seqStr)
if c.httpVersion != "1.1" { if c.httpVersion != "1.1" {
resp, err := c.client.Do(req) resp, err := c.client.Do(req)
if err != nil { if err != nil {
@@ -150,7 +235,6 @@ func (c *DefaultDialerClient) PostPacket(ctx context.Context, url string, body i
} }
c.uploadRawPool.Put(uploadConn) c.uploadRawPool.Put(uploadConn)
} }
return nil return nil
} }
@@ -190,3 +274,45 @@ func (w *WaitReadCloser) Close() error {
close(w.Wait) close(w.Wait)
return nil return nil
} }
func ApplyMetaToRequest(options *option.V2RayXHTTPBaseOptions, req *http.Request, sessionId string, seqStr string) {
sessionPlacement := options.GetNormalizedSessionPlacement()
seqPlacement := options.GetNormalizedSeqPlacement()
sessionKey := options.GetNormalizedSessionKey()
seqKey := options.GetNormalizedSeqKey()
if sessionId != "" {
switch sessionPlacement {
case option.PlacementPath:
req.URL.Path = appendToPath(req.URL.Path, sessionId)
case option.PlacementQuery:
q := req.URL.Query()
q.Set(sessionKey, sessionId)
req.URL.RawQuery = q.Encode()
case option.PlacementHeader:
req.Header.Set(sessionKey, sessionId)
case option.PlacementCookie:
req.AddCookie(&http.Cookie{Name: sessionKey, Value: sessionId})
}
}
if seqStr != "" {
switch seqPlacement {
case option.PlacementPath:
req.URL.Path = appendToPath(req.URL.Path, seqStr)
case option.PlacementQuery:
q := req.URL.Query()
q.Set(seqKey, seqStr)
req.URL.RawQuery = q.Encode()
case option.PlacementHeader:
req.Header.Set(seqKey, seqStr)
case option.PlacementCookie:
req.AddCookie(&http.Cookie{Name: seqKey, Value: seqStr})
}
}
}
func appendToPath(path, value string) string {
if strings.HasSuffix(path, "/") {
return path + value
}
return path + "/" + value
}

View File

@@ -3,6 +3,8 @@ package xhttp
import ( import (
"bytes" "bytes"
"context" "context"
"encoding/base64"
"fmt"
"io" "io"
"net" "net"
"net/http" "net/http"
@@ -17,13 +19,11 @@ import (
"github.com/sagernet/quic-go/http3" "github.com/sagernet/quic-go/http3"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/tls" "github.com/sagernet/sing-box/common/tls"
"github.com/sagernet/sing-box/common/xray/signal/done"
C "github.com/sagernet/sing-box/constant" C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
qtls "github.com/sagernet/sing-quic" qtls "github.com/sagernet/sing-quic"
// qtls "github.com/sagernet/sing-quic"
"github.com/sagernet/sing-box/common/xray/signal/done"
"github.com/sagernet/sing/common" "github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/logger" "github.com/sagernet/sing/common/logger"
M "github.com/sagernet/sing/common/metadata" M "github.com/sagernet/sing/common/metadata"
@@ -99,8 +99,23 @@ func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
return return
} }
writer.Header().Set("Access-Control-Allow-Origin", "*") writer.Header().Set("Access-Control-Allow-Origin", "*")
writer.Header().Set("Access-Control-Allow-Methods", "GET, POST") writer.Header().Set("Access-Control-Allow-Methods", "*")
writer.Header().Set("X-Padding", strings.Repeat("X", int(s.options.GetNormalizedXPaddingBytes().Rand()))) length := int(s.options.GetNormalizedXPaddingBytes().Rand())
config := XPaddingConfig{Length: length}
if s.options.XPaddingObfsMode {
config.Placement = XPaddingPlacement{
Placement: s.options.XPaddingPlacement,
Key: s.options.XPaddingKey,
Header: s.options.XPaddingHeader,
}
config.Method = PaddingMethod(s.options.XPaddingMethod)
} else {
config.Placement = XPaddingPlacement{
Placement: option.PlacementHeader,
Header: "X-Padding",
}
}
ApplyXPaddingToHeader(writer.Header(), config)
validRange := s.options.GetNormalizedXPaddingBytes() validRange := s.options.GetNormalizedXPaddingBytes()
paddingLength := 0 paddingLength := 0
referrer := request.Header.Get("Referer") referrer := request.Header.Get("Referer")
@@ -117,11 +132,7 @@ func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
writer.WriteHeader(http.StatusBadRequest) writer.WriteHeader(http.StatusBadRequest)
return return
} }
sessionId := "" sessionId, seqStr := ExtractMetaFromRequest(s.options, request, s.path)
subpath := strings.Split(request.URL.Path[len(s.path):], "/")
if len(subpath) > 0 {
sessionId = subpath[0]
}
if sessionId == "" && s.options.Mode != "" && s.options.Mode != "auto" && s.options.Mode != "stream-one" && s.options.Mode != "stream-up" { if sessionId == "" && s.options.Mode != "" && s.options.Mode != "auto" && s.options.Mode != "stream-one" && s.options.Mode != "stream-up" {
s.logger.ErrorContext(request.Context(), "stream-one mode is not allowed") s.logger.ErrorContext(request.Context(), "stream-one mode is not allowed")
writer.WriteHeader(http.StatusBadRequest) writer.WriteHeader(http.StatusBadRequest)
@@ -154,12 +165,25 @@ func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
currentSession = s.upsertSession(sessionId) currentSession = s.upsertSession(sessionId)
} }
scMaxEachPostBytes := int(s.options.GetNormalizedScMaxEachPostBytes().To) scMaxEachPostBytes := int(s.options.GetNormalizedScMaxEachPostBytes().To)
if request.Method == "POST" && sessionId != "" { // stream-up, packet-up uplinkHTTPMethod := s.options.GetNormalizedUplinkHTTPMethod()
seq := "" isUplinkRequest := false
if len(subpath) > 1 { if uplinkHTTPMethod != "GET" && request.Method == uplinkHTTPMethod {
seq = subpath[1] isUplinkRequest = true
}
uplinkDataPlacement := s.options.GetNormalizedUplinkDataPlacement()
uplinkDataKey := s.options.UplinkDataKey
switch uplinkDataPlacement {
case option.PlacementHeader:
if request.Header.Get(uplinkDataKey+"-Upstream") == "1" {
isUplinkRequest = true
} }
if seq == "" { case option.PlacementCookie:
if c, _ := request.Cookie(uplinkDataKey + "_upstream"); c != nil && c.Value == "1" {
isUplinkRequest = true
}
}
if isUplinkRequest && sessionId != "" { // stream-up, packet-up
if seqStr == "" {
if s.options.Mode != "" && s.options.Mode != "auto" && s.options.Mode != "stream-up" { if s.options.Mode != "" && s.options.Mode != "auto" && s.options.Mode != "stream-up" {
s.logger.ErrorContext(request.Context(), "stream-up mode is not allowed") s.logger.ErrorContext(request.Context(), "stream-up mode is not allowed")
writer.WriteHeader(http.StatusBadRequest) writer.WriteHeader(http.StatusBadRequest)
@@ -181,6 +205,7 @@ func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
writer.Header().Set("Cache-Control", "no-store") writer.Header().Set("Cache-Control", "no-store")
writer.WriteHeader(http.StatusOK) writer.WriteHeader(http.StatusOK)
scStreamUpServerSecs := s.options.GetNormalizedScStreamUpServerSecs() scStreamUpServerSecs := s.options.GetNormalizedScStreamUpServerSecs()
referrer := request.Header.Get("Referer")
if referrer != "" && scStreamUpServerSecs.To > 0 { if referrer != "" && scStreamUpServerSecs.To > 0 {
go func() { go func() {
for { for {
@@ -205,7 +230,55 @@ func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
writer.WriteHeader(http.StatusBadRequest) writer.WriteHeader(http.StatusBadRequest)
return return
} }
payload, err := io.ReadAll(io.LimitReader(request.Body, int64(scMaxEachPostBytes)+1)) var payload []byte
if uplinkDataPlacement != option.PlacementBody {
var encodedStr string
switch uplinkDataPlacement {
case option.PlacementHeader:
dataLenStr := request.Header.Get(uplinkDataKey + "-Length")
if dataLenStr != "" {
dataLen, _ := strconv.Atoi(dataLenStr)
var chunks []string
i := 0
for {
chunk := request.Header.Get(fmt.Sprintf("%s-%d", uplinkDataKey, i))
if chunk == "" {
break
}
chunks = append(chunks, chunk)
i++
}
encodedStr = strings.Join(chunks, "")
if len(encodedStr) != dataLen {
encodedStr = ""
}
}
case option.PlacementCookie:
var chunks []string
i := 0
for {
cookieName := fmt.Sprintf("%s_%d", uplinkDataKey, i)
if c, _ := request.Cookie(cookieName); c != nil {
chunks = append(chunks, c.Value)
i++
} else {
break
}
}
if len(chunks) > 0 {
encodedStr = strings.Join(chunks, "")
}
}
if encodedStr != "" {
payload, err = base64.RawURLEncoding.DecodeString(encodedStr)
} else {
s.logger.ErrorContext(request.Context(), err, "failed to extract data from key "+uplinkDataKey+" placed in "+uplinkDataPlacement)
writer.WriteHeader(http.StatusInternalServerError)
return
}
} else {
payload, err = io.ReadAll(io.LimitReader(request.Body, int64(scMaxEachPostBytes)+1))
}
if len(payload) > scMaxEachPostBytes { if len(payload) > scMaxEachPostBytes {
s.logger.ErrorContext(request.Context(), "Too large upload. scMaxEachPostBytes is set to ", scMaxEachPostBytes, "but request size exceed it. Adjust scMaxEachPostBytes on the server to be at least as large as client.") s.logger.ErrorContext(request.Context(), "Too large upload. scMaxEachPostBytes is set to ", scMaxEachPostBytes, "but request size exceed it. Adjust scMaxEachPostBytes on the server to be at least as large as client.")
writer.WriteHeader(http.StatusRequestEntityTooLarge) writer.WriteHeader(http.StatusRequestEntityTooLarge)
@@ -216,7 +289,7 @@ func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
writer.WriteHeader(http.StatusInternalServerError) writer.WriteHeader(http.StatusInternalServerError)
return return
} }
seqInt, err := strconv.ParseUint(seq, 10, 64) seq, err := strconv.ParseUint(seqStr, 10, 64)
if err != nil { if err != nil {
s.logger.InfoContext(request.Context(), err, "failed to upload (ParseUint)") s.logger.InfoContext(request.Context(), err, "failed to upload (ParseUint)")
writer.WriteHeader(http.StatusInternalServerError) writer.WriteHeader(http.StatusInternalServerError)
@@ -224,7 +297,7 @@ func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
} }
err = currentSession.uploadQueue.Push(Packet{ err = currentSession.uploadQueue.Push(Packet{
Payload: payload, Payload: payload,
Seq: seqInt, Seq: seq,
}) })
if err != nil { if err != nil {
s.logger.InfoContext(request.Context(), err, "failed to upload (PushPayload)") s.logger.InfoContext(request.Context(), err, "failed to upload (PushPayload)")
@@ -352,3 +425,41 @@ func (s *Server) upsertSession(sessionId string) *httpSession {
}() }()
return session return session
} }
func ExtractMetaFromRequest(options *option.V2RayXHTTPOptions, req *http.Request, path string) (sessionId string, seqStr string) {
sessionPlacement := options.GetNormalizedSessionPlacement()
seqPlacement := options.GetNormalizedSeqPlacement()
sessionKey := options.GetNormalizedSessionKey()
seqKey := options.GetNormalizedSeqKey()
if sessionPlacement == option.PlacementPath && seqPlacement == option.PlacementPath {
subpath := strings.Split(req.URL.Path[len(path):], "/")
if len(subpath) > 0 {
sessionId = subpath[0]
}
if len(subpath) > 1 {
seqStr = subpath[1]
}
return sessionId, seqStr
}
switch sessionPlacement {
case option.PlacementQuery:
sessionId = req.URL.Query().Get(sessionKey)
case option.PlacementHeader:
sessionId = req.Header.Get(sessionKey)
case option.PlacementCookie:
if cookie, e := req.Cookie(sessionKey); e == nil {
sessionId = cookie.Value
}
}
switch seqPlacement {
case option.PlacementQuery:
seqStr = req.URL.Query().Get(seqKey)
case option.PlacementHeader:
seqStr = req.Header.Get(seqKey)
case option.PlacementCookie:
if cookie, e := req.Cookie(seqKey); e == nil {
seqStr = cookie.Value
}
}
return sessionId, seqStr
}

View File

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