package splithttp import ( "encoding/base64" "fmt" "io" "net/http" "strings" "github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common/crypto" "github.com/xtls/xray-core/common/utils" "github.com/xtls/xray-core/transport/internet" ) func (c *Config) GetNormalizedPath() string { pathAndQuery := strings.SplitN(c.Path, "?", 2) path := pathAndQuery[0] if path == "" || path[0] != '/' { path = "/" + path } if path[len(path)-1] != '/' { path = path + "/" } return path } func (c *Config) GetNormalizedQuery() string { pathAndQuery := strings.SplitN(c.Path, "?", 2) query := "" if len(pathAndQuery) > 1 { query = pathAndQuery[1] } /* if query != "" { query += "&" } query += "x_version=" + core.Version() */ return query } func (c *Config) GetRequestHeader() http.Header { header := http.Header{} for k, v := range c.Headers { header.Add(k, v) } utils.TryDefaultHeadersWith(header, "fetch") return header } func (c *Config) GetRequestHeaderWithPayload(payload []byte) http.Header { header := c.GetRequestHeader() key := c.UplinkDataKey encodedData := base64.RawURLEncoding.EncodeToString(payload) for i := 0; len(encodedData) > 0; i++ { chunkSize := min(int(c.GetNormalizedUplinkChunkSize().rand()), len(encodedData)) chunk := encodedData[:chunkSize] encodedData = encodedData[chunkSize:] headerKey := fmt.Sprintf("%s-%d", key, i) header.Set(headerKey, chunk) } return header } func (c *Config) GetRequestCookiesWithPayload(payload []byte) []*http.Cookie { cookies := []*http.Cookie{} key := c.UplinkDataKey encodedData := base64.RawURLEncoding.EncodeToString(payload) for i := 0; len(encodedData) > 0; i++ { chunkSize := min(int(c.GetNormalizedUplinkChunkSize().rand()), len(encodedData)) chunk := encodedData[:chunkSize] encodedData = encodedData[chunkSize:] cookieName := fmt.Sprintf("%s_%d", key, i) cookies = append(cookies, &http.Cookie{Name: cookieName, Value: chunk}) } return cookies } func (c *Config) WriteResponseHeader(writer http.ResponseWriter, requestMethod string, requestHeader http.Header) { // CORS headers for the browser dialer if origin := requestHeader.Get("Origin"); origin == "" { writer.Header().Set("Access-Control-Allow-Origin", "*") } else { // Chrome says: The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'. writer.Header().Set("Access-Control-Allow-Origin", origin) } if c.GetNormalizedSessionPlacement() == PlacementCookie || c.GetNormalizedSeqPlacement() == PlacementCookie || c.XPaddingPlacement == PlacementCookie || c.GetNormalizedUplinkDataPlacement() == PlacementCookie { writer.Header().Set("Access-Control-Allow-Credentials", "true") } if requestMethod == "OPTIONS" { requestedMethod := requestHeader.Get("Access-Control-Request-Method") if requestedMethod != "" { writer.Header().Set("Access-Control-Allow-Methods", requestedMethod) } else { writer.Header().Set("Access-Control-Allow-Methods", "*") } requestedHeaders := requestHeader.Get("Access-Control-Request-Headers") if requestedHeaders == "" { writer.Header().Set("Access-Control-Allow-Headers", "*") } else { writer.Header().Set("Access-Control-Allow-Headers", requestedHeaders) } } } func (c *Config) GetNormalizedUplinkHTTPMethod() string { if c.UplinkHTTPMethod == "" { return "POST" } return c.UplinkHTTPMethod } func (c *Config) GetNormalizedScMaxEachPostBytes() RangeConfig { if c.ScMaxEachPostBytes == nil || c.ScMaxEachPostBytes.To == 0 { return RangeConfig{ From: 1000000, To: 1000000, } } return *c.ScMaxEachPostBytes } func (c *Config) GetNormalizedScMinPostsIntervalMs() RangeConfig { if c.ScMinPostsIntervalMs == nil || c.ScMinPostsIntervalMs.To == 0 { return RangeConfig{ From: 30, To: 30, } } return *c.ScMinPostsIntervalMs } func (c *Config) GetNormalizedScMaxBufferedPosts() int { if c.ScMaxBufferedPosts == 0 { return 30 } 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.ScStreamUpServerSecs } func (c *Config) GetNormalizedUplinkChunkSize() RangeConfig { if c.UplinkChunkSize == nil || c.UplinkChunkSize.To == 0 { switch c.UplinkDataPlacement { case PlacementCookie: return RangeConfig{ From: 2 * 1024, // 2 KiB To: 3 * 1024, // 3 KiB } case PlacementHeader: return RangeConfig{ From: 3 * 1000, // 3 KB To: 4 * 1000, // 4 KB } default: return c.GetNormalizedScMaxEachPostBytes() } } else if c.UplinkChunkSize.From < 64 { return RangeConfig{ From: 64, To: max(64, c.UplinkChunkSize.To), } } return *c.UplinkChunkSize } func (c *Config) GetNormalizedServerMaxHeaderBytes() int { if c.ServerMaxHeaderBytes <= 0 { return 8192 } else { return int(c.ServerMaxHeaderBytes) } } func (c *Config) GetNormalizedSessionPlacement() string { if c.SessionPlacement == "" { return PlacementPath } return c.SessionPlacement } func (c *Config) GetNormalizedSeqPlacement() string { if c.SeqPlacement == "" { return PlacementPath } return c.SeqPlacement } func (c *Config) GetNormalizedUplinkDataPlacement() string { if c.UplinkDataPlacement == "" { return PlacementBody } return c.UplinkDataPlacement } func (c *Config) 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 *Config) GetNormalizedSeqKey() string { if c.SeqKey != "" { return c.SeqKey } switch c.GetNormalizedSeqPlacement() { case PlacementHeader: return "X-Seq" case PlacementCookie, PlacementQuery: return "x_seq" default: return "" } } func (c *Config) ApplyMetaToRequest(req *http.Request, sessionId string, seqStr string) { sessionPlacement := c.GetNormalizedSessionPlacement() seqPlacement := c.GetNormalizedSeqPlacement() sessionKey := c.GetNormalizedSessionKey() seqKey := c.GetNormalizedSeqKey() if sessionId != "" { switch sessionPlacement { case PlacementPath: req.URL.Path = appendToPath(req.URL.Path, sessionId) case PlacementQuery: q := req.URL.Query() q.Set(sessionKey, sessionId) req.URL.RawQuery = q.Encode() case PlacementHeader: req.Header.Set(sessionKey, sessionId) case PlacementCookie: req.AddCookie(&http.Cookie{Name: sessionKey, Value: sessionId}) } } if seqStr != "" { switch seqPlacement { case PlacementPath: req.URL.Path = appendToPath(req.URL.Path, seqStr) case PlacementQuery: q := req.URL.Query() q.Set(seqKey, seqStr) req.URL.RawQuery = q.Encode() case PlacementHeader: req.Header.Set(seqKey, seqStr) case PlacementCookie: req.AddCookie(&http.Cookie{Name: seqKey, Value: seqStr}) } } } func (c *Config) FillStreamRequest(request *http.Request, sessionId string, seqStr string) { request.Header = c.GetRequestHeader() length := int(c.GetNormalizedXPaddingBytes().rand()) config := XPaddingConfig{Length: length} if c.XPaddingObfsMode { config.Placement = XPaddingPlacement{ Placement: c.XPaddingPlacement, Key: c.XPaddingKey, Header: c.XPaddingHeader, RawURL: request.URL.String(), } config.Method = PaddingMethod(c.XPaddingMethod) } else { config.Placement = XPaddingPlacement{ Placement: PlacementQueryInHeader, Key: "x_padding", Header: "Referer", RawURL: request.URL.String(), } } c.ApplyXPaddingToRequest(request, config) c.ApplyMetaToRequest(request, sessionId, "") if request.Body != nil && !c.NoGRPCHeader { // stream-up/one request.Header.Set("Content-Type", "application/grpc") } } func (c *Config) FillPacketRequest(request *http.Request, sessionId string, seqStr string) error { dataPlacement := c.GetNormalizedUplinkDataPlacement() if dataPlacement == PlacementBody || dataPlacement == PlacementAuto { request.Header = c.GetRequestHeader() } else { var data []byte var err error if request.Body != nil { data, err = io.ReadAll(request.Body) if err != nil { return err } } request.Body = nil request.ContentLength = 0 switch dataPlacement { case PlacementHeader: request.Header = c.GetRequestHeaderWithPayload(data) case PlacementCookie: request.Header = c.GetRequestHeader() for _, cookie := range c.GetRequestCookiesWithPayload(data) { request.AddCookie(cookie) } } } length := int(c.GetNormalizedXPaddingBytes().rand()) config := XPaddingConfig{Length: length} if c.XPaddingObfsMode { config.Placement = XPaddingPlacement{ Placement: c.XPaddingPlacement, Key: c.XPaddingKey, Header: c.XPaddingHeader, RawURL: request.URL.String(), } config.Method = PaddingMethod(c.XPaddingMethod) } else { config.Placement = XPaddingPlacement{ Placement: PlacementQueryInHeader, Key: "x_padding", Header: "Referer", RawURL: request.URL.String(), } } c.ApplyXPaddingToRequest(request, config) c.ApplyMetaToRequest(request, sessionId, seqStr) return nil } func (c *Config) ExtractMetaFromRequest(req *http.Request, path string) (sessionId string, seqStr string) { sessionPlacement := c.GetNormalizedSessionPlacement() seqPlacement := c.GetNormalizedSeqPlacement() sessionKey := c.GetNormalizedSessionKey() seqKey := c.GetNormalizedSeqKey() var subpath []string pathPart := 0 if sessionPlacement == PlacementPath || seqPlacement == PlacementPath { subpath = strings.Split(req.URL.Path[len(path):], "/") } switch sessionPlacement { case PlacementPath: if len(subpath) > pathPart { sessionId = subpath[pathPart] pathPart += 1 } case PlacementQuery: sessionId = req.URL.Query().Get(sessionKey) case PlacementHeader: sessionId = req.Header.Get(sessionKey) case PlacementCookie: if cookie, e := req.Cookie(sessionKey); e == nil { sessionId = cookie.Value } } switch seqPlacement { case PlacementPath: if len(subpath) > pathPart { seqStr = subpath[pathPart] pathPart += 1 } case PlacementQuery: seqStr = req.URL.Query().Get(seqKey) case PlacementHeader: seqStr = req.Header.Get(seqKey) case PlacementCookie: if cookie, e := req.Cookie(seqKey); e == nil { seqStr = cookie.Value } } return sessionId, seqStr } func (m *XmuxConfig) GetNormalizedMaxConcurrency() RangeConfig { if m.MaxConcurrency == nil { return RangeConfig{ From: 0, To: 0, } } return *m.MaxConcurrency } func (m *XmuxConfig) GetNormalizedMaxConnections() RangeConfig { if m.MaxConnections == nil { return RangeConfig{ From: 0, To: 0, } } return *m.MaxConnections } func (m *XmuxConfig) GetNormalizedCMaxReuseTimes() RangeConfig { if m.CMaxReuseTimes == nil { return RangeConfig{ From: 0, To: 0, } } return *m.CMaxReuseTimes } func (m *XmuxConfig) GetNormalizedHMaxRequestTimes() RangeConfig { if m.HMaxRequestTimes == nil { return RangeConfig{ From: 0, To: 0, } } return *m.HMaxRequestTimes } func (m *XmuxConfig) GetNormalizedHMaxReusableSecs() RangeConfig { if m.HMaxReusableSecs == nil { return RangeConfig{ From: 0, To: 0, } } return *m.HMaxReusableSecs } func init() { common.Must(internet.RegisterProtocolConfigCreator(protocolName, func() interface{} { return new(Config) })) } func (c RangeConfig) rand() int32 { return int32(crypto.RandBetween(int64(c.From), int64(c.To))) } func appendToPath(path, value string) string { if strings.HasSuffix(path, "/") { return path + value } return path + "/" + value }