Update xhttp

This commit is contained in:
Shtorm
2026-02-22 14:48:52 +03:00
parent 9e4eb52a82
commit a3aaf2cb92
9 changed files with 863 additions and 104 deletions

View File

@@ -3,6 +3,8 @@ package xhttp
import (
"bytes"
"context"
"encoding/base64"
"fmt"
"io"
"net"
"net/http"
@@ -17,13 +19,11 @@ import (
"github.com/sagernet/quic-go/http3"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/tls"
"github.com/sagernet/sing-box/common/xray/signal/done"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
qtls "github.com/sagernet/sing-quic"
// qtls "github.com/sagernet/sing-quic"
"github.com/sagernet/sing-box/common/xray/signal/done"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/logger"
M "github.com/sagernet/sing/common/metadata"
@@ -99,8 +99,23 @@ func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
return
}
writer.Header().Set("Access-Control-Allow-Origin", "*")
writer.Header().Set("Access-Control-Allow-Methods", "GET, POST")
writer.Header().Set("X-Padding", strings.Repeat("X", int(s.options.GetNormalizedXPaddingBytes().Rand())))
writer.Header().Set("Access-Control-Allow-Methods", "*")
length := int(s.options.GetNormalizedXPaddingBytes().Rand())
config := XPaddingConfig{Length: length}
if s.options.XPaddingObfsMode {
config.Placement = XPaddingPlacement{
Placement: s.options.XPaddingPlacement,
Key: s.options.XPaddingKey,
Header: s.options.XPaddingHeader,
}
config.Method = PaddingMethod(s.options.XPaddingMethod)
} else {
config.Placement = XPaddingPlacement{
Placement: option.PlacementHeader,
Header: "X-Padding",
}
}
ApplyXPaddingToHeader(writer.Header(), config)
validRange := s.options.GetNormalizedXPaddingBytes()
paddingLength := 0
referrer := request.Header.Get("Referer")
@@ -117,11 +132,7 @@ func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
writer.WriteHeader(http.StatusBadRequest)
return
}
sessionId := ""
subpath := strings.Split(request.URL.Path[len(s.path):], "/")
if len(subpath) > 0 {
sessionId = subpath[0]
}
sessionId, seqStr := ExtractMetaFromRequest(s.options, request, s.path)
if sessionId == "" && s.options.Mode != "" && s.options.Mode != "auto" && s.options.Mode != "stream-one" && s.options.Mode != "stream-up" {
s.logger.ErrorContext(request.Context(), "stream-one mode is not allowed")
writer.WriteHeader(http.StatusBadRequest)
@@ -154,12 +165,25 @@ func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
currentSession = s.upsertSession(sessionId)
}
scMaxEachPostBytes := int(s.options.GetNormalizedScMaxEachPostBytes().To)
if request.Method == "POST" && sessionId != "" { // stream-up, packet-up
seq := ""
if len(subpath) > 1 {
seq = subpath[1]
uplinkHTTPMethod := s.options.GetNormalizedUplinkHTTPMethod()
isUplinkRequest := false
if uplinkHTTPMethod != "GET" && request.Method == uplinkHTTPMethod {
isUplinkRequest = true
}
uplinkDataPlacement := s.options.GetNormalizedUplinkDataPlacement()
uplinkDataKey := s.options.UplinkDataKey
switch uplinkDataPlacement {
case option.PlacementHeader:
if request.Header.Get(uplinkDataKey+"-Upstream") == "1" {
isUplinkRequest = true
}
if seq == "" {
case option.PlacementCookie:
if c, _ := request.Cookie(uplinkDataKey + "_upstream"); c != nil && c.Value == "1" {
isUplinkRequest = true
}
}
if isUplinkRequest && sessionId != "" { // stream-up, packet-up
if seqStr == "" {
if s.options.Mode != "" && s.options.Mode != "auto" && s.options.Mode != "stream-up" {
s.logger.ErrorContext(request.Context(), "stream-up mode is not allowed")
writer.WriteHeader(http.StatusBadRequest)
@@ -181,6 +205,7 @@ func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
writer.Header().Set("Cache-Control", "no-store")
writer.WriteHeader(http.StatusOK)
scStreamUpServerSecs := s.options.GetNormalizedScStreamUpServerSecs()
referrer := request.Header.Get("Referer")
if referrer != "" && scStreamUpServerSecs.To > 0 {
go func() {
for {
@@ -205,7 +230,55 @@ func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
writer.WriteHeader(http.StatusBadRequest)
return
}
payload, err := io.ReadAll(io.LimitReader(request.Body, int64(scMaxEachPostBytes)+1))
var payload []byte
if uplinkDataPlacement != option.PlacementBody {
var encodedStr string
switch uplinkDataPlacement {
case option.PlacementHeader:
dataLenStr := request.Header.Get(uplinkDataKey + "-Length")
if dataLenStr != "" {
dataLen, _ := strconv.Atoi(dataLenStr)
var chunks []string
i := 0
for {
chunk := request.Header.Get(fmt.Sprintf("%s-%d", uplinkDataKey, i))
if chunk == "" {
break
}
chunks = append(chunks, chunk)
i++
}
encodedStr = strings.Join(chunks, "")
if len(encodedStr) != dataLen {
encodedStr = ""
}
}
case option.PlacementCookie:
var chunks []string
i := 0
for {
cookieName := fmt.Sprintf("%s_%d", uplinkDataKey, i)
if c, _ := request.Cookie(cookieName); c != nil {
chunks = append(chunks, c.Value)
i++
} else {
break
}
}
if len(chunks) > 0 {
encodedStr = strings.Join(chunks, "")
}
}
if encodedStr != "" {
payload, err = base64.RawURLEncoding.DecodeString(encodedStr)
} else {
s.logger.ErrorContext(request.Context(), err, "failed to extract data from key "+uplinkDataKey+" placed in "+uplinkDataPlacement)
writer.WriteHeader(http.StatusInternalServerError)
return
}
} else {
payload, err = io.ReadAll(io.LimitReader(request.Body, int64(scMaxEachPostBytes)+1))
}
if len(payload) > scMaxEachPostBytes {
s.logger.ErrorContext(request.Context(), "Too large upload. scMaxEachPostBytes is set to ", scMaxEachPostBytes, "but request size exceed it. Adjust scMaxEachPostBytes on the server to be at least as large as client.")
writer.WriteHeader(http.StatusRequestEntityTooLarge)
@@ -216,7 +289,7 @@ func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
writer.WriteHeader(http.StatusInternalServerError)
return
}
seqInt, err := strconv.ParseUint(seq, 10, 64)
seq, err := strconv.ParseUint(seqStr, 10, 64)
if err != nil {
s.logger.InfoContext(request.Context(), err, "failed to upload (ParseUint)")
writer.WriteHeader(http.StatusInternalServerError)
@@ -224,7 +297,7 @@ func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
}
err = currentSession.uploadQueue.Push(Packet{
Payload: payload,
Seq: seqInt,
Seq: seq,
})
if err != nil {
s.logger.InfoContext(request.Context(), err, "failed to upload (PushPayload)")
@@ -352,3 +425,41 @@ func (s *Server) upsertSession(sessionId string) *httpSession {
}()
return session
}
func ExtractMetaFromRequest(options *option.V2RayXHTTPOptions, req *http.Request, path string) (sessionId string, seqStr string) {
sessionPlacement := options.GetNormalizedSessionPlacement()
seqPlacement := options.GetNormalizedSeqPlacement()
sessionKey := options.GetNormalizedSessionKey()
seqKey := options.GetNormalizedSeqKey()
if sessionPlacement == option.PlacementPath && seqPlacement == option.PlacementPath {
subpath := strings.Split(req.URL.Path[len(path):], "/")
if len(subpath) > 0 {
sessionId = subpath[0]
}
if len(subpath) > 1 {
seqStr = subpath[1]
}
return sessionId, seqStr
}
switch sessionPlacement {
case option.PlacementQuery:
sessionId = req.URL.Query().Get(sessionKey)
case option.PlacementHeader:
sessionId = req.Header.Get(sessionKey)
case option.PlacementCookie:
if cookie, e := req.Cookie(sessionKey); e == nil {
sessionId = cookie.Value
}
}
switch seqPlacement {
case option.PlacementQuery:
seqStr = req.URL.Query().Get(seqKey)
case option.PlacementHeader:
seqStr = req.Header.Get(seqKey)
case option.PlacementCookie:
if cookie, e := req.Cookie(seqKey); e == nil {
seqStr = cookie.Value
}
}
return sessionId, seqStr
}