Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions infra/conf/transport_internet.go
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,7 @@ type SplitHTTPConfig struct {
ScMaxEachPostBytes Int32Range `json:"scMaxEachPostBytes"`
ScMinPostsIntervalMs Int32Range `json:"scMinPostsIntervalMs"`
ScMaxBufferedPosts int64 `json:"scMaxBufferedPosts"`
ScStreamUpServerSecs Int32Range `json:"scStreamUpServerSecs"`
Xmux XmuxConfig `json:"xmux"`
DownloadSettings *StreamConfig `json:"downloadSettings"`
Extra json.RawMessage `json:"extra"`
Expand Down Expand Up @@ -280,6 +281,10 @@ func (c *SplitHTTPConfig) Build() (proto.Message, error) {
}
}

if c.XPaddingBytes != (Int32Range{}) && (c.XPaddingBytes.From <= 0 || c.XPaddingBytes.To <= 0) {
return nil, errors.New("xPaddingBytes cannot be disabled")
}

if c.Xmux.MaxConnections.To > 0 && c.Xmux.MaxConcurrency.To > 0 {
return nil, errors.New("maxConnections cannot be specified together with maxConcurrency")
}
Expand All @@ -303,6 +308,7 @@ func (c *SplitHTTPConfig) Build() (proto.Message, error) {
ScMaxEachPostBytes: newRangeConfig(c.ScMaxEachPostBytes),
ScMinPostsIntervalMs: newRangeConfig(c.ScMinPostsIntervalMs),
ScMaxBufferedPosts: c.ScMaxBufferedPosts,
ScStreamUpServerSecs: newRangeConfig(c.ScStreamUpServerSecs),
Xmux: &splithttp.XmuxConfig{
MaxConcurrency: newRangeConfig(c.Xmux.MaxConcurrency),
MaxConnections: newRangeConfig(c.Xmux.MaxConnections),
Expand Down
4 changes: 2 additions & 2 deletions transport/internet/splithttp/browser_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ func (c *BrowserDialerClient) OpenStream(ctx context.Context, url string, body i
return nil, nil, nil, errors.New("bidirectional streaming for browser dialer not implemented yet")
}

conn, err := browser_dialer.DialGet(url, c.transportConfig.GetRequestHeader())
conn, err := browser_dialer.DialGet(url, c.transportConfig.GetRequestHeader(url))
dummyAddr := &gonet.IPAddr{}
if err != nil {
return nil, dummyAddr, dummyAddr, err
Expand All @@ -39,7 +39,7 @@ func (c *BrowserDialerClient) PostPacket(ctx context.Context, url string, body i
return err
}

err = browser_dialer.DialPost(url, c.transportConfig.GetRequestHeader(), bytes)
err = browser_dialer.DialPost(url, c.transportConfig.GetRequestHeader(url), bytes)
if err != nil {
return err
}
Expand Down
8 changes: 4 additions & 4 deletions transport/internet/splithttp/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ func (c *DefaultDialerClient) OpenStream(ctx context.Context, url string, body i
method = "POST" // stream-up/one
}
req, _ := http.NewRequestWithContext(context.WithoutCancel(ctx), method, url, body)
req.Header = c.transportConfig.GetRequestHeader()
req.Header = c.transportConfig.GetRequestHeader(url)
if method == "POST" && !c.transportConfig.NoGRPCHeader {
req.Header.Set("Content-Type", "application/grpc")
}
Expand All @@ -69,10 +69,10 @@ func (c *DefaultDialerClient) OpenStream(ctx context.Context, url string, body i
go func() {
resp, err := c.client.Do(req)
if err != nil {
if !uploadOnly {
if !uploadOnly { // stream-down is enough
c.closed = true
errors.LogInfoInner(ctx, err, "failed to "+method+" "+url)
}
errors.LogInfoInner(ctx, err, "failed to "+method+" "+url)
gotConn.Close()
wrc.Close()
return
Expand All @@ -99,7 +99,7 @@ func (c *DefaultDialerClient) PostPacket(ctx context.Context, url string, body i
return err
}
req.ContentLength = contentLength
req.Header = c.transportConfig.GetRequestHeader()
req.Header = c.transportConfig.GetRequestHeader(url)

if c.httpVersion != "1.1" {
resp, err := c.client.Do(req)
Expand Down
62 changes: 28 additions & 34 deletions transport/internet/splithttp/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,9 @@ import (
"strings"

"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/core"
"github.com/xtls/xray-core/transport/internet"
)

const paddingQuery = "x_padding"

func (c *Config) GetNormalizedPath() string {
pathAndQuery := strings.SplitN(c.Path, "?", 2)
path := pathAndQuery[0]
Expand All @@ -37,54 +34,40 @@ func (c *Config) GetNormalizedQuery() string {
query = pathAndQuery[1]
}

if query != "" {
query += "&"
}
query += "x_version=" + core.Version()
/*
if query != "" {
query += "&"
}
query += "x_version=" + core.Version()
*/

return query
}

func (c *Config) GetRequestHeader() http.Header {
func (c *Config) GetRequestHeader(rawURL string) http.Header {
header := http.Header{}
for k, v := range c.Headers {
header.Add(k, v)
}

paddingLen := c.GetNormalizedXPaddingBytes().rand()
if paddingLen > 0 {
query, err := url.ParseQuery(c.GetNormalizedQuery())
if err != nil {
query = url.Values{}
}
// 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' 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.
query.Set(paddingQuery, strings.Repeat("X", int(paddingLen)))

referrer := url.URL{
Scheme: "https", // maybe http actually, but this part is not being checked
Host: c.Host,
Path: c.GetNormalizedPath(),
RawQuery: query.Encode(),
}
u, _ := url.Parse(rawURL)
// 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' 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())

header.Set("Referer", referrer.String())
}
return header
}

func (c *Config) WriteResponseHeader(writer http.ResponseWriter) {
// CORS headers for the browser dialer
writer.Header().Set("Access-Control-Allow-Origin", "*")
writer.Header().Set("Access-Control-Allow-Methods", "GET, POST")
writer.Header().Set("X-Version", core.Version())
paddingLen := c.GetNormalizedXPaddingBytes().rand()
if paddingLen > 0 {
writer.Header().Set("X-Padding", strings.Repeat("X", int(paddingLen)))
}
// writer.Header().Set("X-Version", core.Version())
writer.Header().Set("X-Padding", strings.Repeat("X", int(c.GetNormalizedXPaddingBytes().rand())))
}

func (c *Config) GetNormalizedXPaddingBytes() RangeConfig {
Expand Down Expand Up @@ -128,6 +111,17 @@ func (c *Config) GetNormalizedScMaxBufferedPosts() int {
return int(c.ScMaxBufferedPosts)
}

func (c *Config) GetNormalizedScStreamUpServerSecs() RangeConfig {
if c.ScStreamUpServerSecs == nil || c.ScStreamUpServerSecs.To == 0 {
return RangeConfig{
From: 20,
To: 80,
}
}

return *c.ScMinPostsIntervalMs
}

func (m *XmuxConfig) GetNormalizedMaxConcurrency() RangeConfig {
if m.MaxConcurrency == nil {
return RangeConfig{
Expand Down
79 changes: 47 additions & 32 deletions transport/internet/splithttp/config.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions transport/internet/splithttp/config.proto
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ message Config {
RangeConfig scMaxEachPostBytes = 8;
RangeConfig scMinPostsIntervalMs = 9;
int64 scMaxBufferedPosts = 10;
XmuxConfig xmux = 11;
xray.transport.internet.StreamConfig downloadSettings = 12;
RangeConfig scStreamUpServerSecs = 11;
XmuxConfig xmux = 12;
xray.transport.internet.StreamConfig downloadSettings = 13;
}
56 changes: 33 additions & 23 deletions transport/internet/splithttp/hub.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,29 +104,28 @@ func (h *requestHandler) ServeHTTP(writer http.ResponseWriter, request *http.Req

h.config.WriteResponseHeader(writer)

clientVer := []int{0, 0, 0}
x_version := strings.Split(request.URL.Query().Get("x_version"), ".")
for j := 0; j < 3 && len(x_version) > j; j++ {
clientVer[j], _ = strconv.Atoi(x_version[j])
}
/*
clientVer := []int{0, 0, 0}
x_version := strings.Split(request.URL.Query().Get("x_version"), ".")
for j := 0; j < 3 && len(x_version) > j; j++ {
clientVer[j], _ = strconv.Atoi(x_version[j])
}
*/

validRange := h.config.GetNormalizedXPaddingBytes()
paddingLength := -1
paddingLength := 0

if referrerPadding := request.Header.Get("Referer"); referrerPadding != "" {
// Browser dialer cannot control the host part of referrer header, so only check the query
if referrerURL, err := url.Parse(referrerPadding); err == nil {
if query := referrerURL.Query(); query.Has(paddingQuery) {
paddingLength = len(query.Get(paddingQuery))
}
referrer := request.Header.Get("Referer")
if referrer != "" {
if referrerURL, err := url.Parse(referrer); err == nil {
// Browser dialer cannot control the host part of referrer header, so only check the query
paddingLength = len(referrerURL.Query().Get("x_padding"))
}
} else {
paddingLength = len(request.URL.Query().Get("x_padding"))
}

if paddingLength == -1 {
paddingLength = len(request.URL.Query().Get(paddingQuery))
}

if validRange.To > 0 && (int32(paddingLength) < validRange.From || int32(paddingLength) > validRange.To) {
if int32(paddingLength) < validRange.From || int32(paddingLength) > validRange.To {
errors.LogInfo(context.Background(), "invalid x_padding length:", int32(paddingLength))
writer.WriteHeader(http.StatusBadRequest)
return
Expand Down Expand Up @@ -181,13 +180,24 @@ func (h *requestHandler) ServeHTTP(writer http.ResponseWriter, request *http.Req
errors.LogInfoInner(context.Background(), err, "failed to upload (PushReader)")
writer.WriteHeader(http.StatusConflict)
} else {
writer.Header().Set("X-Accel-Buffering", "no")
writer.Header().Set("Cache-Control", "no-store")
writer.WriteHeader(http.StatusOK)
if request.ProtoMajor != 1 && len(clientVer) > 0 && clientVer[0] >= 25 {
paddingLen := h.config.GetNormalizedXPaddingBytes().rand()
if paddingLen > 0 {
writer.Write(bytes.Repeat([]byte{'0'}, int(paddingLen)))
}
writer.(http.Flusher).Flush()
scStreamUpServerSecs := h.config.GetNormalizedScStreamUpServerSecs()
if referrer != "" && scStreamUpServerSecs.To > 0 {
go func() {
defer func() {
recover()
}()
for {
_, err := writer.Write(bytes.Repeat([]byte{'X'}, int(h.config.GetNormalizedXPaddingBytes().rand())))
if err != nil {
break
}
writer.(http.Flusher).Flush()
time.Sleep(time.Duration(scStreamUpServerSecs.rand()) * time.Second)
}
}()
}
<-request.Context().Done()
}
Expand Down
Loading