mirror of
https://github.com/shtorm-7/sing-box-extended.git
synced 2026-06-22 18:44:13 +03:00
Update xhttp
This commit is contained in:
@@ -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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
28
common/xray/utils/browser.go
Normal file
28
common/xray/utils/browser.go
Normal 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"
|
||||||
24
common/xray/utils/padding.go
Normal file
24
common/xray/utils/padding.go
Normal 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)
|
||||||
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"`
|
||||||
|
|||||||
@@ -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()),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
268
transport/v2rayxhttp/xpadding.go
Normal file
268
transport/v2rayxhttp/xpadding.go
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user