2026-05-11 00:59:35 +03:00
package xhttp
import (
"encoding/base64"
"fmt"
"io"
2026-06-26 01:25:57 +03:00
"math/rand/v2"
2026-05-11 00:59:35 +03:00
"net/http"
"github.com/sagernet/sing-box/common/xray/buf"
"github.com/sagernet/sing-box/common/xray/utils"
2026-06-26 01:25:57 +03:00
"github.com/sagernet/sing-box/common/xray/uuid"
2026-05-11 00:59:35 +03:00
"github.com/sagernet/sing-box/option"
)
2026-06-26 01:25:57 +03:00
// PredefinedTable maps named charsets to their alphabets for session ID generation.
var PredefinedTable = map [ string ] string {
"ALPHABET" : "ABCDEFGHIJKLMNOPQRSTUVWXYZ" ,
"Alphabet" : "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" ,
"BASE36" : "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" ,
"Base62" : "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" ,
"HEX" : "0123456789ABCDEF" ,
"alphabet" : "abcdefghijklmnopqrstuvwxyz" ,
"base36" : "0123456789abcdefghijklmnopqrstuvwxyz" ,
"hex" : "0123456789abcdef" ,
"number" : "0123456789" ,
}
func GenerateSessionID ( options * option . V2RayXHTTPBaseOptions ) string {
length := options . SessionIDLength . Rand ()
table := options . SessionIDTable
if predefined , ok := PredefinedTable [ table ]; ok {
table = predefined
}
if table != "" && length > 0 {
id := make ([] byte , length )
for i := range id {
id [ i ] = table [ rand . N ( len ( table ))]
}
return string ( id )
}
newUUID := uuid . New ()
return newUUID . String ()
}
2026-05-11 00:59:35 +03:00
func FillStreamRequest ( request * http . Request , sessionId string , seqStr string , options * option . V2RayXHTTPBaseOptions ) {
request . Header = options . GetRequestHeader ()
2026-06-26 01:25:57 +03:00
length := options . GetNormalizedXPaddingBytes (). Rand ()
2026-05-11 00:59:35 +03:00
config := XPaddingConfig { Length : length }
if options . XPaddingObfsMode {
config . Placement = XPaddingPlacement {
Placement : options . XPaddingPlacement ,
Key : options . XPaddingKey ,
Header : options . XPaddingHeader ,
RawURL : request . URL . String (),
}
config . Method = PaddingMethod ( options . XPaddingMethod )
} else {
config . Placement = XPaddingPlacement {
Placement : option . PlacementQueryInHeader ,
Key : "x_padding" ,
Header : "Referer" ,
RawURL : request . URL . String (),
}
}
ApplyXPaddingToRequest ( request , config )
ApplyMetaToRequest ( options , request , sessionId , "" )
if request . Body != nil && ! options . NoGRPCHeader { // stream-up/one
request . Header . Set ( "Content-Type" , "application/grpc" )
}
}
func FillPacketRequest ( request * http . Request , sessionId string , seqStr string , payload buf . MultiBuffer , options * option . V2RayXHTTPBaseOptions ) error {
dataPlacement := options . GetNormalizedUplinkDataPlacement ()
if dataPlacement == option . PlacementBody || dataPlacement == option . PlacementAuto {
request . Header = options . GetRequestHeader ()
request . Body = io . NopCloser ( & buf . MultiBufferContainer { MultiBuffer : payload })
request . ContentLength = int64 ( payload . Len ())
} else {
data := make ([] byte , payload . Len ())
payload . Copy ( data )
buf . ReleaseMulti ( payload )
switch dataPlacement {
case option . PlacementHeader :
request . Header = GetRequestHeaderWithPayload ( data , options )
case option . PlacementCookie :
request . Header = options . GetRequestHeader ()
for _ , cookie := range GetRequestCookiesWithPayload ( data , options ) {
request . AddCookie ( cookie )
}
}
}
2026-06-26 01:25:57 +03:00
length := options . GetNormalizedXPaddingBytes (). Rand ()
2026-05-11 00:59:35 +03:00
config := XPaddingConfig { Length : length }
if options . XPaddingObfsMode {
config . Placement = XPaddingPlacement {
Placement : options . XPaddingPlacement ,
Key : options . XPaddingKey ,
Header : options . XPaddingHeader ,
RawURL : request . URL . String (),
}
config . Method = PaddingMethod ( options . XPaddingMethod )
} else {
config . Placement = XPaddingPlacement {
Placement : option . PlacementQueryInHeader ,
Key : "x_padding" ,
Header : "Referer" ,
RawURL : request . URL . String (),
}
}
ApplyXPaddingToRequest ( request , config )
ApplyMetaToRequest ( options , request , sessionId , seqStr )
return nil
}
func WriteResponseHeader ( writer http . ResponseWriter , requestMethod string , requestHeader http . Header , options * option . V2RayXHTTPOptions ) {
// 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 options . GetNormalizedSessionPlacement () == option . PlacementCookie ||
options . GetNormalizedSeqPlacement () == option . PlacementCookie ||
options . XPaddingPlacement == option . PlacementCookie ||
options . GetNormalizedUplinkDataPlacement () == option . 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 GetRequestHeader ( options * option . V2RayXHTTPBaseOptions ) http . Header {
header := http . Header {}
for k , v := range options . Headers {
header . Add ( k , v )
}
utils . TryDefaultHeadersWith ( header , "fetch" )
return header
}
func GetRequestHeaderWithPayload ( payload [] byte , options * option . V2RayXHTTPBaseOptions ) http . Header {
header := GetRequestHeader ( options )
key := options . UplinkDataKey
encodedData := base64 . RawURLEncoding . EncodeToString ( payload )
for i := 0 ; len ( encodedData ) > 0 ; i ++ {
2026-06-26 01:25:57 +03:00
chunkSize := min ( options . GetNormalizedUplinkChunkSize (). Rand (), len ( encodedData ))
2026-05-11 00:59:35 +03:00
chunk := encodedData [: chunkSize ]
encodedData = encodedData [ chunkSize :]
headerKey := fmt . Sprintf ( "%s-%d" , key , i )
header . Set ( headerKey , chunk )
}
return header
}
func GetRequestCookiesWithPayload ( payload [] byte , options * option . V2RayXHTTPBaseOptions ) [] * http . Cookie {
cookies := [] * http . Cookie {}
key := options . UplinkDataKey
encodedData := base64 . RawURLEncoding . EncodeToString ( payload )
for i := 0 ; len ( encodedData ) > 0 ; i ++ {
2026-06-26 01:25:57 +03:00
chunkSize := min ( options . GetNormalizedUplinkChunkSize (). Rand (), len ( encodedData ))
2026-05-11 00:59:35 +03:00
chunk := encodedData [: chunkSize ]
encodedData = encodedData [ chunkSize :]
cookieName := fmt . Sprintf ( "%s_%d" , key , i )
cookies = append ( cookies , & http . Cookie { Name : cookieName , Value : chunk })
}
return cookies
}