Skip to content

Commit

Permalink
XHTTP server: Add scStreamUpServerSecs, enabled by default
Browse files Browse the repository at this point in the history
  • Loading branch information
RPRX authored Jan 19, 2025
1 parent f4fd8b8 commit 37d631e
Show file tree
Hide file tree
Showing 7 changed files with 123 additions and 97 deletions.
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

0 comments on commit 37d631e

Please sign in to comment.