Skip to content

Commit ca9a902

Browse files
authored
XHTTP server: Add scStreamUpServerSecs, enabled by default (#4306)
Fixes #4113 (comment)
1 parent f4fd8b8 commit ca9a902

7 files changed

Lines changed: 123 additions & 97 deletions

File tree

infra/conf/transport_internet.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,7 @@ type SplitHTTPConfig struct {
231231
ScMaxEachPostBytes Int32Range `json:"scMaxEachPostBytes"`
232232
ScMinPostsIntervalMs Int32Range `json:"scMinPostsIntervalMs"`
233233
ScMaxBufferedPosts int64 `json:"scMaxBufferedPosts"`
234+
ScStreamUpServerSecs Int32Range `json:"scStreamUpServerSecs"`
234235
Xmux XmuxConfig `json:"xmux"`
235236
DownloadSettings *StreamConfig `json:"downloadSettings"`
236237
Extra json.RawMessage `json:"extra"`
@@ -280,6 +281,10 @@ func (c *SplitHTTPConfig) Build() (proto.Message, error) {
280281
}
281282
}
282283

284+
if c.XPaddingBytes != (Int32Range{}) && (c.XPaddingBytes.From <= 0 || c.XPaddingBytes.To <= 0) {
285+
return nil, errors.New("xPaddingBytes cannot be disabled")
286+
}
287+
283288
if c.Xmux.MaxConnections.To > 0 && c.Xmux.MaxConcurrency.To > 0 {
284289
return nil, errors.New("maxConnections cannot be specified together with maxConcurrency")
285290
}
@@ -303,6 +308,7 @@ func (c *SplitHTTPConfig) Build() (proto.Message, error) {
303308
ScMaxEachPostBytes: newRangeConfig(c.ScMaxEachPostBytes),
304309
ScMinPostsIntervalMs: newRangeConfig(c.ScMinPostsIntervalMs),
305310
ScMaxBufferedPosts: c.ScMaxBufferedPosts,
311+
ScStreamUpServerSecs: newRangeConfig(c.ScStreamUpServerSecs),
306312
Xmux: &splithttp.XmuxConfig{
307313
MaxConcurrency: newRangeConfig(c.Xmux.MaxConcurrency),
308314
MaxConnections: newRangeConfig(c.Xmux.MaxConnections),

transport/internet/splithttp/browser_client.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ func (c *BrowserDialerClient) OpenStream(ctx context.Context, url string, body i
2424
return nil, nil, nil, errors.New("bidirectional streaming for browser dialer not implemented yet")
2525
}
2626

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

42-
err = browser_dialer.DialPost(url, c.transportConfig.GetRequestHeader(), bytes)
42+
err = browser_dialer.DialPost(url, c.transportConfig.GetRequestHeader(url), bytes)
4343
if err != nil {
4444
return err
4545
}

transport/internet/splithttp/client.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ func (c *DefaultDialerClient) OpenStream(ctx context.Context, url string, body i
6060
method = "POST" // stream-up/one
6161
}
6262
req, _ := http.NewRequestWithContext(context.WithoutCancel(ctx), method, url, body)
63-
req.Header = c.transportConfig.GetRequestHeader()
63+
req.Header = c.transportConfig.GetRequestHeader(url)
6464
if method == "POST" && !c.transportConfig.NoGRPCHeader {
6565
req.Header.Set("Content-Type", "application/grpc")
6666
}
@@ -69,10 +69,10 @@ func (c *DefaultDialerClient) OpenStream(ctx context.Context, url string, body i
6969
go func() {
7070
resp, err := c.client.Do(req)
7171
if err != nil {
72-
if !uploadOnly {
72+
if !uploadOnly { // stream-down is enough
7373
c.closed = true
74+
errors.LogInfoInner(ctx, err, "failed to "+method+" "+url)
7475
}
75-
errors.LogInfoInner(ctx, err, "failed to "+method+" "+url)
7676
gotConn.Close()
7777
wrc.Close()
7878
return
@@ -99,7 +99,7 @@ func (c *DefaultDialerClient) PostPacket(ctx context.Context, url string, body i
9999
return err
100100
}
101101
req.ContentLength = contentLength
102-
req.Header = c.transportConfig.GetRequestHeader()
102+
req.Header = c.transportConfig.GetRequestHeader(url)
103103

104104
if c.httpVersion != "1.1" {
105105
resp, err := c.client.Do(req)

transport/internet/splithttp/config.go

Lines changed: 28 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,9 @@ import (
88
"strings"
99

1010
"github.com/xtls/xray-core/common"
11-
"github.com/xtls/xray-core/core"
1211
"github.com/xtls/xray-core/transport/internet"
1312
)
1413

15-
const paddingQuery = "x_padding"
16-
1714
func (c *Config) GetNormalizedPath() string {
1815
pathAndQuery := strings.SplitN(c.Path, "?", 2)
1916
path := pathAndQuery[0]
@@ -37,54 +34,40 @@ func (c *Config) GetNormalizedQuery() string {
3734
query = pathAndQuery[1]
3835
}
3936

40-
if query != "" {
41-
query += "&"
42-
}
43-
query += "x_version=" + core.Version()
37+
/*
38+
if query != "" {
39+
query += "&"
40+
}
41+
query += "x_version=" + core.Version()
42+
*/
4443

4544
return query
4645
}
4746

48-
func (c *Config) GetRequestHeader() http.Header {
47+
func (c *Config) GetRequestHeader(rawURL string) http.Header {
4948
header := http.Header{}
5049
for k, v := range c.Headers {
5150
header.Add(k, v)
5251
}
5352

54-
paddingLen := c.GetNormalizedXPaddingBytes().rand()
55-
if paddingLen > 0 {
56-
query, err := url.ParseQuery(c.GetNormalizedQuery())
57-
if err != nil {
58-
query = url.Values{}
59-
}
60-
// https://www.rfc-editor.org/rfc/rfc7541.html#appendix-B
61-
// h2's HPACK Header Compression feature employs a huffman encoding using a static table.
62-
// 'X' is assigned an 8 bit code, so HPACK compression won't change actual padding length on the wire.
63-
// https://www.rfc-editor.org/rfc/rfc9204.html#section-4.1.2-2
64-
// h3's similar QPACK feature uses the same huffman table.
65-
query.Set(paddingQuery, strings.Repeat("X", int(paddingLen)))
66-
67-
referrer := url.URL{
68-
Scheme: "https", // maybe http actually, but this part is not being checked
69-
Host: c.Host,
70-
Path: c.GetNormalizedPath(),
71-
RawQuery: query.Encode(),
72-
}
53+
u, _ := url.Parse(rawURL)
54+
// https://www.rfc-editor.org/rfc/rfc7541.html#appendix-B
55+
// h2's HPACK Header Compression feature employs a huffman encoding using a static table.
56+
// 'X' is assigned an 8 bit code, so HPACK compression won't change actual padding length on the wire.
57+
// https://www.rfc-editor.org/rfc/rfc9204.html#section-4.1.2-2
58+
// h3's similar QPACK feature uses the same huffman table.
59+
u.RawQuery = "x_padding=" + strings.Repeat("X", int(c.GetNormalizedXPaddingBytes().rand()))
60+
header.Set("Referer", u.String())
7361

74-
header.Set("Referer", referrer.String())
75-
}
7662
return header
7763
}
7864

7965
func (c *Config) WriteResponseHeader(writer http.ResponseWriter) {
8066
// CORS headers for the browser dialer
8167
writer.Header().Set("Access-Control-Allow-Origin", "*")
8268
writer.Header().Set("Access-Control-Allow-Methods", "GET, POST")
83-
writer.Header().Set("X-Version", core.Version())
84-
paddingLen := c.GetNormalizedXPaddingBytes().rand()
85-
if paddingLen > 0 {
86-
writer.Header().Set("X-Padding", strings.Repeat("X", int(paddingLen)))
87-
}
69+
// writer.Header().Set("X-Version", core.Version())
70+
writer.Header().Set("X-Padding", strings.Repeat("X", int(c.GetNormalizedXPaddingBytes().rand())))
8871
}
8972

9073
func (c *Config) GetNormalizedXPaddingBytes() RangeConfig {
@@ -128,6 +111,17 @@ func (c *Config) GetNormalizedScMaxBufferedPosts() int {
128111
return int(c.ScMaxBufferedPosts)
129112
}
130113

114+
func (c *Config) GetNormalizedScStreamUpServerSecs() RangeConfig {
115+
if c.ScStreamUpServerSecs == nil || c.ScStreamUpServerSecs.To == 0 {
116+
return RangeConfig{
117+
From: 20,
118+
To: 80,
119+
}
120+
}
121+
122+
return *c.ScMinPostsIntervalMs
123+
}
124+
131125
func (m *XmuxConfig) GetNormalizedMaxConcurrency() RangeConfig {
132126
if m.MaxConcurrency == nil {
133127
return RangeConfig{

transport/internet/splithttp/config.pb.go

Lines changed: 47 additions & 32 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

transport/internet/splithttp/config.proto

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ message Config {
3333
RangeConfig scMaxEachPostBytes = 8;
3434
RangeConfig scMinPostsIntervalMs = 9;
3535
int64 scMaxBufferedPosts = 10;
36-
XmuxConfig xmux = 11;
37-
xray.transport.internet.StreamConfig downloadSettings = 12;
36+
RangeConfig scStreamUpServerSecs = 11;
37+
XmuxConfig xmux = 12;
38+
xray.transport.internet.StreamConfig downloadSettings = 13;
3839
}

transport/internet/splithttp/hub.go

Lines changed: 33 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -104,29 +104,28 @@ func (h *requestHandler) ServeHTTP(writer http.ResponseWriter, request *http.Req
104104

105105
h.config.WriteResponseHeader(writer)
106106

107-
clientVer := []int{0, 0, 0}
108-
x_version := strings.Split(request.URL.Query().Get("x_version"), ".")
109-
for j := 0; j < 3 && len(x_version) > j; j++ {
110-
clientVer[j], _ = strconv.Atoi(x_version[j])
111-
}
107+
/*
108+
clientVer := []int{0, 0, 0}
109+
x_version := strings.Split(request.URL.Query().Get("x_version"), ".")
110+
for j := 0; j < 3 && len(x_version) > j; j++ {
111+
clientVer[j], _ = strconv.Atoi(x_version[j])
112+
}
113+
*/
112114

113115
validRange := h.config.GetNormalizedXPaddingBytes()
114-
paddingLength := -1
116+
paddingLength := 0
115117

116-
if referrerPadding := request.Header.Get("Referer"); referrerPadding != "" {
117-
// Browser dialer cannot control the host part of referrer header, so only check the query
118-
if referrerURL, err := url.Parse(referrerPadding); err == nil {
119-
if query := referrerURL.Query(); query.Has(paddingQuery) {
120-
paddingLength = len(query.Get(paddingQuery))
121-
}
118+
referrer := request.Header.Get("Referer")
119+
if referrer != "" {
120+
if referrerURL, err := url.Parse(referrer); err == nil {
121+
// Browser dialer cannot control the host part of referrer header, so only check the query
122+
paddingLength = len(referrerURL.Query().Get("x_padding"))
122123
}
124+
} else {
125+
paddingLength = len(request.URL.Query().Get("x_padding"))
123126
}
124127

125-
if paddingLength == -1 {
126-
paddingLength = len(request.URL.Query().Get(paddingQuery))
127-
}
128-
129-
if validRange.To > 0 && (int32(paddingLength) < validRange.From || int32(paddingLength) > validRange.To) {
128+
if int32(paddingLength) < validRange.From || int32(paddingLength) > validRange.To {
130129
errors.LogInfo(context.Background(), "invalid x_padding length:", int32(paddingLength))
131130
writer.WriteHeader(http.StatusBadRequest)
132131
return
@@ -181,13 +180,24 @@ func (h *requestHandler) ServeHTTP(writer http.ResponseWriter, request *http.Req
181180
errors.LogInfoInner(context.Background(), err, "failed to upload (PushReader)")
182181
writer.WriteHeader(http.StatusConflict)
183182
} else {
183+
writer.Header().Set("X-Accel-Buffering", "no")
184+
writer.Header().Set("Cache-Control", "no-store")
184185
writer.WriteHeader(http.StatusOK)
185-
if request.ProtoMajor != 1 && len(clientVer) > 0 && clientVer[0] >= 25 {
186-
paddingLen := h.config.GetNormalizedXPaddingBytes().rand()
187-
if paddingLen > 0 {
188-
writer.Write(bytes.Repeat([]byte{'0'}, int(paddingLen)))
189-
}
190-
writer.(http.Flusher).Flush()
186+
scStreamUpServerSecs := h.config.GetNormalizedScStreamUpServerSecs()
187+
if referrer != "" && scStreamUpServerSecs.To > 0 {
188+
go func() {
189+
defer func() {
190+
recover()
191+
}()
192+
for {
193+
_, err := writer.Write(bytes.Repeat([]byte{'X'}, int(h.config.GetNormalizedXPaddingBytes().rand())))
194+
if err != nil {
195+
break
196+
}
197+
writer.(http.Flusher).Flush()
198+
time.Sleep(time.Duration(scStreamUpServerSecs.rand()) * time.Second)
199+
}
200+
}()
191201
}
192202
<-request.Context().Done()
193203
}

0 commit comments

Comments
 (0)