Skip to content

Commit

Permalink
SplitHTTP: Replace responseOkPadding with xPaddingBytes (#3643)
Browse files Browse the repository at this point in the history
  • Loading branch information
mmmray authored Aug 10, 2024
1 parent f650d87 commit a3b306a
Show file tree
Hide file tree
Showing 8 changed files with 119 additions and 106 deletions.
4 changes: 2 additions & 2 deletions infra/conf/transport_internet.go
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ type SplitHTTPConfig struct {
ScMaxEachPostBytes *Int32Range `json:"scMaxEachPostBytes"`
ScMinPostsIntervalMs *Int32Range `json:"scMinPostsIntervalMs"`
NoSSEHeader bool `json:"noSSEHeader"`
ResponseOkPadding *Int32Range `json:"responseOkPadding"`
XPaddingBytes *Int32Range `json:"xPaddingBytes"`
}

func splithttpNewRandRangeConfig(input *Int32Range) *splithttp.RandRangeConfig {
Expand Down Expand Up @@ -265,7 +265,7 @@ func (c *SplitHTTPConfig) Build() (proto.Message, error) {
ScMaxEachPostBytes: splithttpNewRandRangeConfig(c.ScMaxEachPostBytes),
ScMinPostsIntervalMs: splithttpNewRandRangeConfig(c.ScMinPostsIntervalMs),
NoSSEHeader: c.NoSSEHeader,
ResponseOkPadding: splithttpNewRandRangeConfig(c.ResponseOkPadding),
XPaddingBytes: splithttpNewRandRangeConfig(c.XPaddingBytes),
}
return config, nil
}
Expand Down
43 changes: 34 additions & 9 deletions transport/internet/splithttp/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,32 +10,57 @@ import (
"github.com/xtls/xray-core/transport/internet"
)

func (c *Config) GetNormalizedPath(addPath string, addQuery bool) string {
func (c *Config) GetNormalizedPath() string {
pathAndQuery := strings.SplitN(c.Path, "?", 2)
path := pathAndQuery[0]
query := ""
if len(pathAndQuery) > 1 && addQuery {
query = "?" + pathAndQuery[1]
}

if path == "" || path[0] != '/' {
path = "/" + path
}

if path[len(path)-1] != '/' {
path = path + "/"
}

return path + addPath + query
return path
}

func (c *Config) GetNormalizedQuery() string {
pathAndQuery := strings.SplitN(c.Path, "?", 2)
query := ""

if len(pathAndQuery) > 1 {
query = pathAndQuery[1]
}

if query != "" {
query += "&"
}

paddingLen := c.GetNormalizedXPaddingBytes().roll()
if paddingLen > 0 {
query += "x_padding=" + strings.Repeat("0", int(paddingLen))
}

return query
}

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

return header
}

func (c *Config) WriteResponseHeader(writer http.ResponseWriter) {
paddingLen := c.GetNormalizedXPaddingBytes().roll()
if paddingLen > 0 {
writer.Header().Set("X-Padding", strings.Repeat("0", int(paddingLen)))
}
}

func (c *Config) GetNormalizedScMaxConcurrentPosts() RandRangeConfig {
if c.ScMaxConcurrentPosts == nil || c.ScMaxConcurrentPosts.To == 0 {
return RandRangeConfig{
Expand Down Expand Up @@ -69,15 +94,15 @@ func (c *Config) GetNormalizedScMinPostsIntervalMs() RandRangeConfig {
return *c.ScMinPostsIntervalMs
}

func (c *Config) GetNormalizedResponseOkPadding() RandRangeConfig {
if c.ResponseOkPadding == nil || c.ResponseOkPadding.To == 0 {
func (c *Config) GetNormalizedXPaddingBytes() RandRangeConfig {
if c.XPaddingBytes == nil || c.XPaddingBytes.To == 0 {
return RandRangeConfig{
From: 100,
To: 1000,
}
}

return *c.ResponseOkPadding
return *c.XPaddingBytes
}

func init() {
Expand Down
56 changes: 28 additions & 28 deletions transport/internet/splithttp/config.pb.go

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

2 changes: 1 addition & 1 deletion transport/internet/splithttp/config.proto
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ message Config {
RandRangeConfig scMaxEachPostBytes = 5;
RandRangeConfig scMinPostsIntervalMs = 6;
bool noSSEHeader = 7;
RandRangeConfig responseOkPadding = 8;
RandRangeConfig xPaddingBytes = 8;
}

message RandRangeConfig {
Expand Down
37 changes: 2 additions & 35 deletions transport/internet/splithttp/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,41 +11,8 @@ func Test_GetNormalizedPath(t *testing.T) {
Path: "/?world",
}

path := c.GetNormalizedPath("hello", true)
if path != "/hello?world" {
t.Error("Unexpected: ", path)
}
}

func Test_GetNormalizedPath2(t *testing.T) {
c := Config{
Path: "?world",
}

path := c.GetNormalizedPath("hello", true)
if path != "/hello?world" {
t.Error("Unexpected: ", path)
}
}

func Test_GetNormalizedPath3(t *testing.T) {
c := Config{
Path: "hello?world",
}

path := c.GetNormalizedPath("", true)
if path != "/hello/?world" {
t.Error("Unexpected: ", path)
}
}

func Test_GetNormalizedPath4(t *testing.T) {
c := Config{
Path: "hello?world",
}

path := c.GetNormalizedPath("", false)
if path != "/hello/" {
path := c.GetNormalizedPath()
if path != "/" {
t.Error("Unexpected: ", path)
}
}
50 changes: 35 additions & 15 deletions transport/internet/splithttp/dialer.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package splithttp

import (
"bytes"
"context"
gotls "crypto/tls"
"io"
Expand Down Expand Up @@ -217,8 +218,8 @@ func Dial(ctx context.Context, dest net.Destination, streamSettings *internet.Me
}

sessionIdUuid := uuid.New()
requestURL.Path = transportConfiguration.GetNormalizedPath(sessionIdUuid.String(), true)
baseURL := requestURL.String()
requestURL.Path = transportConfiguration.GetNormalizedPath() + sessionIdUuid.String()
requestURL.RawQuery = transportConfiguration.GetNormalizedQuery()

httpClient := getHTTPClient(ctx, dest, streamSettings)

Expand Down Expand Up @@ -247,9 +248,16 @@ func Dial(ctx context.Context, dest net.Destination, streamSettings *internet.Me
go func() {
defer requestsLimiter.Signal()

// this intentionally makes a shallow-copy of the struct so we
// can reassign Path (potentially concurrently)
url := requestURL
url.Path += "/" + strconv.FormatInt(seq, 10)
// reassign query to get different padding
url.RawQuery = transportConfiguration.GetNormalizedQuery()

err := httpClient.SendUploadRequest(
context.WithoutCancel(ctx),
baseURL+"/"+strconv.FormatInt(seq, 10),
url.String(),
&buf.MultiBufferContainer{MultiBuffer: chunk},
int64(chunk.Len()),
)
Expand All @@ -271,26 +279,38 @@ func Dial(ctx context.Context, dest net.Destination, streamSettings *internet.Me
}
}()

lazyRawDownload, remoteAddr, localAddr, err := httpClient.OpenDownload(context.WithoutCancel(ctx), baseURL)
lazyRawDownload, remoteAddr, localAddr, err := httpClient.OpenDownload(context.WithoutCancel(ctx), requestURL.String())
if err != nil {
return nil, err
}

lazyDownload := &LazyReader{
CreateReader: func() (io.ReadCloser, error) {
// skip "ooooooooook" response
trashHeader := []byte{0}
for {
_, err := io.ReadFull(lazyRawDownload, trashHeader)
if err != nil {
return nil, errors.New("failed to read initial response").Base(err)
}
if trashHeader[0] == 'k' {
break
}
// skip "ok" response
trashHeader := []byte{0, 0}
_, err := io.ReadFull(lazyRawDownload, trashHeader)
if err != nil {
return nil, errors.New("failed to read initial response").Base(err)
}

if bytes.Equal(trashHeader, []byte("ok")) {
return lazyRawDownload, nil
}

return lazyRawDownload, nil
// we read some garbage byte that may not have been "ok" at
// all. return a reader that replays what we have read so far
reader := io.MultiReader(
bytes.NewReader(trashHeader),
lazyRawDownload,
)
readCloser := struct {
io.Reader
io.Closer
}{
Reader: reader,
Closer: lazyRawDownload,
}
return readCloser, nil
},
}

Expand Down
18 changes: 9 additions & 9 deletions transport/internet/splithttp/hub.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,6 @@ func (h *requestHandler) ServeHTTP(writer http.ResponseWriter, request *http.Req

currentSession := h.upsertSession(sessionId)
scMaxEachPostBytes := int(h.ln.config.GetNormalizedScMaxEachPostBytes().To)
responseOkPadding := h.ln.config.GetNormalizedResponseOkPadding()

if request.Method == "POST" {
seq := ""
Expand Down Expand Up @@ -170,6 +169,7 @@ func (h *requestHandler) ServeHTTP(writer http.ResponseWriter, request *http.Req
return
}

h.config.WriteResponseHeader(writer)
writer.WriteHeader(http.StatusOK)
} else if request.Method == "GET" {
responseFlusher, ok := writer.(http.Flusher)
Expand All @@ -189,14 +189,14 @@ func (h *requestHandler) ServeHTTP(writer http.ResponseWriter, request *http.Req
writer.Header().Set("Content-Type", "text/event-stream")
}

h.config.WriteResponseHeader(writer)

writer.WriteHeader(http.StatusOK)
// send a chunk immediately to enable CDN streaming.
// many CDN buffer the response headers until the origin starts sending
// the body, with no way to turn it off.
padding := int(responseOkPadding.roll())
for i := 0; i < padding; i++ {
writer.Write([]byte("o"))
}
// in earlier versions, this initial body data was used to immediately
// start a 200 OK on all CDN. but xray client since 1.8.16 does not
// actually require an immediate 200 OK, but now requires these
// additional bytes "ok". xray client 1.8.24+ doesn't require "ok"
// anymore, and so this line should be removed in later versions.
writer.Write([]byte("ok"))
responseFlusher.Flush()

Expand Down Expand Up @@ -277,7 +277,7 @@ func ListenSH(ctx context.Context, address net.Address, port net.Port, streamSet
handler := &requestHandler{
config: shSettings,
host: shSettings.Host,
path: shSettings.GetNormalizedPath("", false),
path: shSettings.GetNormalizedPath(),
ln: l,
sessionMu: &sync.Mutex{},
sessions: sync.Map{},
Expand Down
Loading

0 comments on commit a3b306a

Please sign in to comment.