Files
DragonCoreSSH-NewWEB/xray_xhttp.go
T
2026-07-24 10:08:06 -03:00

1653 lines
46 KiB
Go

package main
import (
"container/heap"
"context"
"crypto/tls"
"encoding/base64"
"errors"
"fmt"
"io"
"net"
"net/http"
"os"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
"golang.org/x/net/http2"
"golang.org/x/net/http2/h2c"
)
const (
nativeXHTTPMaxSessionIDBytes = 256
nativeXHTTPMaxSequenceBytes = 20
nativeXHTTPHardMaxHeaderBytes = 256 * 1024
nativeXHTTPHardMaxPostBytes int64 = 16 * 1024 * 1024
nativeXHTTPMaxBufferedPosts = 512
nativeXHTTPMaxBufferedSessionBytes = 16 * 1024 * 1024
nativeXHTTPMaxBufferedGlobalBytes = 128 * 1024 * 1024
// Tiny/empty packet-up requests still retain queue metadata. Charge a minimum
// amount against the byte budgets so the reassembly queue can be count-unlimited
// without allowing zero-byte packets to grow the heap without bound.
nativeXHTTPMinPacketAccountingBytes int64 = 256
)
var (
nativeXHTTPBufferedBytes atomic.Int64
nativeXHTTPBufferRejected atomic.Int64
errNativeXHTTPUploadBufferFull = errors.New("xhttp upload buffer limit reached")
nativeXHTTPMemoryWait = struct {
sync.Mutex
waiters []*nativeXHTTPMemoryWaiter
head int
queued int
}{}
)
type nativeXHTTPMemoryWaiter struct {
bytes int64
ready chan struct{}
granted bool
}
const (
xhttpPlacementPath = "path"
xhttpPlacementQuery = "query"
xhttpPlacementHeader = "header"
xhttpPlacementCookie = "cookie"
xhttpPlacementBody = "body"
xhttpPlacementAuto = "auto"
)
// isXHTTP reports whether this inbound uses XHTTP/SplitHTTP. Xray historically
// uses both names; the panel uses "xhttp" while upstream registers "splithttp".
func (ib *nativeInbound) isXHTTP() bool {
switch strings.ToLower(ib.transport) {
case "xhttp", "splithttp":
return true
default:
return false
}
}
func normalizeXHTTPPath(path string) string {
path = strings.TrimSpace(path)
if i := strings.Index(path, "?"); i >= 0 {
path = path[:i]
}
if path == "" || path[0] != '/' {
path = "/" + path
}
if !strings.HasSuffix(path, "/") {
path += "/"
}
return path
}
func mergeNativeXHTTPSettings(primary, fallback nativeXHTTPSettingsJSON) nativeXHTTPSettingsJSON {
out := primary
if out.Host == "" {
out.Host = fallback.Host
}
if out.Path == "" {
out.Path = fallback.Path
}
if out.Mode == "" {
out.Mode = fallback.Mode
}
if !out.NoSSEHeader {
out.NoSSEHeader = fallback.NoSSEHeader
}
if out.SessionIDPlacement == "" {
out.SessionIDPlacement = fallback.SessionIDPlacement
}
if out.SessionIDKey == "" {
out.SessionIDKey = fallback.SessionIDKey
}
if out.SeqPlacement == "" {
out.SeqPlacement = fallback.SeqPlacement
}
if out.SeqKey == "" {
out.SeqKey = fallback.SeqKey
}
if out.UplinkDataPlacement == "" {
out.UplinkDataPlacement = fallback.UplinkDataPlacement
}
if out.UplinkDataKey == "" {
out.UplinkDataKey = fallback.UplinkDataKey
}
if out.ScMaxEachPostBytes == nil {
out.ScMaxEachPostBytes = fallback.ScMaxEachPostBytes
}
if out.ScMaxBufferedPosts == 0 {
out.ScMaxBufferedPosts = fallback.ScMaxBufferedPosts
}
if out.ServerMaxHeaderBytes == 0 {
out.ServerMaxHeaderBytes = fallback.ServerMaxHeaderBytes
}
return out
}
func newNativeXHTTPListener(addr string, inbounds []*nativeInbound) (*nativeXHTTPListener, error) {
if len(inbounds) == 0 {
return nil, fmt.Errorf("native xray: shared XHTTP listener %s has no inbounds", addr)
}
group := &nativeXHTTPListener{addr: addr, inbounds: append([]*nativeInbound(nil), inbounds...)}
paths := make(map[string]string, len(inbounds))
for i, ib := range group.inbounds {
if ib == nil || !ib.isXHTTP() {
return nil, fmt.Errorf("native xray: shared XHTTP listener %s contains a non-XHTTP inbound", addr)
}
ib.path = normalizeXHTTPPath(ib.path)
if previous, exists := paths[ib.path]; exists {
return nil, fmt.Errorf("native xray: XHTTP inbounds %q and %q use the same path %s on %s", previous, ib.tag, ib.path, addr)
}
paths[ib.path] = ib.tag
if size := ib.xhttpServerMaxHeaderBytes(); size > group.headerSize {
group.headerSize = size
}
if i == 0 {
group.security = ib.security
group.tlsConfig = ib.tlsConfig
continue
}
if ib.security != group.security {
return nil, fmt.Errorf("native xray: XHTTP inbounds sharing %s must use the same TLS setting", addr)
}
if group.security == "tls" && !sameNativeTLSCertificate(group.tlsConfig, ib.tlsConfig) {
return nil, fmt.Errorf("native xray: XHTTP inbounds sharing %s must use the same TLS certificate", addr)
}
}
if group.security == "tls" && group.tlsConfig == nil {
return nil, fmt.Errorf("native xray: shared XHTTP listener %s has no TLS configuration", addr)
}
return group, nil
}
func sameNativeTLSCertificate(a, b *tls.Config) bool {
if a == nil || b == nil || len(a.Certificates) == 0 || len(b.Certificates) == 0 {
return a == b
}
ac := a.Certificates[0].Certificate
bc := b.Certificates[0].Certificate
if len(ac) == 0 || len(bc) == 0 || len(ac[0]) != len(bc[0]) {
return false
}
return string(ac[0]) == string(bc[0])
}
func (ib *nativeInbound) serveXHTTPListener(ln net.Listener) {
group, err := newNativeXHTTPListener(ln.Addr().String(), []*nativeInbound{ib})
if err != nil {
xrayLogf("native xray: XHTTP listener %q rejected: %v", ib.tag, err)
return
}
group.serve(ln)
}
func (g *nativeXHTTPListener) serve(ln net.Listener) {
defer xrayRecover(fmt.Sprintf("native xray shared XHTTP listener addr=%s", ln.Addr()))
h2s := &http2.Server{
MaxConcurrentStreams: nativeHTTP2MaxConcurrentStreams(),
}
handler := http.Handler(g)
// Official Xray accepts plaintext HTTP/1.1 and h2c on non-TLS XHTTP
// listeners, and negotiates h2/http1 through ALPN on TLS listeners. Without
// h2c, some clients/CDNs can reach the port but the request never reaches the
// XHTTP handler, which makes the proxy look dead with no useful target logs.
if g.security != "tls" {
handler = h2c.NewHandler(g, h2s)
}
srv := &http.Server{
Handler: handler,
ReadHeaderTimeout: 4 * time.Second,
MaxHeaderBytes: g.headerSize,
}
if g.security == "tls" && g.tlsConfig != nil {
srv.TLSConfig = g.tlsConfig
_ = http2.ConfigureServer(srv, h2s)
}
// Each path has its own session namespace and idle sweeper.
stopSweep := make(chan struct{})
defer close(stopSweep)
for _, ib := range g.inbounds {
ib := ib
xrayGo(fmt.Sprintf("native xray XHTTP idle sweeper inbound=%q", ib.tag), func() { ib.sweepXHTTPSessions(stopSweep) })
}
if err := srv.Serve(ln); err != nil && !errors.Is(err, http.ErrServerClosed) && !isListenerClosed(err) {
xrayLogf("native xray: shared XHTTP server on %s stopped: %v", g.addr, err)
}
}
// ServeHTTP picks the longest configured path. This makes a root XHTTP inbound
// coexist with more specific services such as /ssh without allowing the root
// handler to steal the SSH session path.
func (g *nativeXHTTPListener) ServeHTTP(w http.ResponseWriter, r *http.Request) {
var selected *nativeInbound
selectedLen := -1
for _, ib := range g.inbounds {
if _, ok := ib.matchXHTTPPath(r.URL.Path); ok && len(ib.path) > selectedLen {
selected = ib
selectedLen = len(ib.path)
}
}
if selected == nil {
w.WriteHeader(http.StatusNotFound)
return
}
selected.ServeHTTP(w, r)
}
// sweepXHTTPSessions periodically evicts connected XHTTP sessions that have seen
// no traffic (in either direction) within the idle timeout. This is the safety
// net behind the per-request context watch in handleXHTTPDownload; it only ever
// touches sessions whose lastSeen has genuinely gone stale, so an active tunnel
// (which refreshes lastSeen via nativeXHTTPConn.onActivity) is never reaped.
func (ib *nativeInbound) sweepXHTTPSessions(stop <-chan struct{}) {
defer xrayRecover(fmt.Sprintf("native xray XHTTP idle sweeper inbound=%q", ib.tag))
idle := nativeXHTTPIdleTimeout()
if idle <= 0 {
return
}
interval := idle / 4
if interval < 15*time.Second {
interval = 15 * time.Second
}
t := time.NewTicker(interval)
defer t.Stop()
for {
select {
case <-stop:
return
case <-t.C:
ib.reapStaleXHTTPSessions(idle)
}
}
}
func (ib *nativeInbound) reapStaleXHTTPSessions(idle time.Duration) {
now := time.Now()
var stale []*nativeXHTTPSession
ib.xhttpMu.Lock()
for id, s := range ib.xhttpSessions {
s.mu.Lock()
connected := s.connected
last := s.lastSeen
s.mu.Unlock()
// Unconnected sessions have their own 30s reaper; only reap connected
// ones that have gone idle past the timeout.
if connected && now.Sub(last) >= idle {
delete(ib.xhttpSessions, id)
stale = append(stale, s)
}
}
ib.xhttpMu.Unlock()
for _, s := range stale {
xrayTracef("native xray: xhttp idle sweep closing session=%q inbound=%q", s.id, ib.tag)
s.close()
}
}
func (ib *nativeInbound) xhttpServerMaxHeaderBytes() int {
if ib.xhttpMaxHeaderBytes > 0 {
if ib.xhttpMaxHeaderBytes > nativeXHTTPHardMaxHeaderBytes {
return nativeXHTTPHardMaxHeaderBytes
}
return ib.xhttpMaxHeaderBytes
}
// Xray defaults to 8192. Keep a little room for custom headers/cookies used
// by packet-up mode while still preventing unbounded memory use.
return 64 * 1024
}
// ServeHTTP terminates the XHTTP/SplitHTTP transport and exposes the decoded
// byte stream to the VLESS/VMess handlers as a net.Conn.
func (ib *nativeInbound) ServeHTTP(w http.ResponseWriter, r *http.Request) {
defer xrayRecover(fmt.Sprintf("native xray XHTTP request inbound=%q method=%s path=%q remote=%s", ib.tag, r.Method, r.URL.RequestURI(), r.RemoteAddr))
// XHTTP is a VPN transport, not a web API. A single connected user keeps a
// long-lived download handler and can generate many short packet-up handlers.
// Rejecting handlers at an application request ceiling turns normal tunnel
// bursts into 429s and, through CDNs/reverse proxies, intermittent 502s.
// HTTP/2 flow control plus the bounded, cancelable upload queues below provide
// backpressure without applying website rate-limit semantics to tunnel traffic.
if !ib.isXHTTP() {
logNativePreAuthRejection("native xray: xhttp reject inbound=%q reason=not-xhttp method=%s path=%q host=%q remote=%s", ib.tag, r.Method, r.URL.RequestURI(), r.Host, r.RemoteAddr)
xhttpBadRequest(w)
return
}
if !ib.xhttpHostAllowed(r.Host) {
logNativePreAuthRejection("native xray: xhttp reject inbound=%q reason=host method=%s path=%q host=%q want=%q remote=%s", ib.tag, r.Method, r.URL.RequestURI(), r.Host, ib.xhttpHost, r.RemoteAddr)
w.WriteHeader(http.StatusNotFound)
return
}
base, ok := ib.matchXHTTPPath(r.URL.Path)
if !ok {
logNativePreAuthRejection("native xray: xhttp reject inbound=%q reason=path method=%s path=%q want=%q host=%q remote=%s", ib.tag, r.Method, r.URL.RequestURI(), ib.path, r.Host, r.RemoteAddr)
w.WriteHeader(http.StatusNotFound)
return
}
ib.writeXHTTPCommonHeaders(w, r)
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusOK)
return
}
releaseRequest, ok := acquireNativeXHTTPRequest()
if !ok {
logNativeLimitRejection("simultaneous XHTTP handlers", &nativeXHTTPRejected, nativeXHTTPRequestLimit())
w.Header().Set("Retry-After", "1")
http.Error(w, "xhttp transport temporarily busy", http.StatusServiceUnavailable)
return
}
defer releaseRequest()
sessionID, seqStr := ib.extractXHTTPMeta(r, base)
if len(sessionID) > nativeXHTTPMaxSessionIDBytes || len(seqStr) > nativeXHTTPMaxSequenceBytes {
logNativePreAuthRejection("native xray: xhttp reject inbound=%q reason=metadata-size remote=%s", ib.tag, r.RemoteAddr)
xhttpBadRequest(w)
return
}
mode := ib.normalizedXHTTPMode()
xrayTracef("native xray: xhttp request inbound=%q method=%s proto=%s path=%q host=%q session=%q seq=%q len=%d mode=%s remote=%s", ib.tag, r.Method, r.Proto, r.URL.RequestURI(), r.Host, sessionID, seqStr, r.ContentLength, mode, r.RemoteAddr)
if sessionID == "" && mode != "" && mode != "auto" && mode != "stream-one" && mode != "stream-up" {
http.Error(w, "stream-one mode is not allowed", http.StatusBadRequest)
return
}
isUplinkRequest := true
if r.Method == http.MethodGet {
isUplinkRequest = seqStr != ""
}
if isUplinkRequest && sessionID != "" { // stream-up, packet-up
sess := ib.upsertXHTTPSession(w, sessionID)
if sess == nil {
return
}
if seqStr == "" {
ib.handleXHTTPStreamUpload(w, r, sess)
return
}
ib.handleXHTTPPacketUpload(w, r, sess, seqStr)
return
}
if r.Method == http.MethodGet || sessionID == "" { // stream-down, stream-one
if sessionID != "" {
sess := ib.upsertXHTTPSession(w, sessionID)
if sess == nil {
return
}
ib.handleXHTTPDownload(w, r, sess, sessionID)
return
}
if r.Body == nil || (r.ContentLength == 0 && len(r.TransferEncoding) == 0) {
xhttpBadRequest(w)
return
}
ib.handleXHTTPStreamOne(w, r)
return
}
w.Header().Set("Allow", "GET, POST, PUT, PATCH, OPTIONS")
w.WriteHeader(http.StatusMethodNotAllowed)
}
func xhttpBadRequest(w http.ResponseWriter) {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
}
func (ib *nativeInbound) normalizedXHTTPMode() string {
mode := strings.ToLower(strings.TrimSpace(ib.xhttpMode))
if mode == "" {
return "auto"
}
return mode
}
func isXHTTPUploadMethod(method string) bool {
switch method {
case http.MethodPost, http.MethodPut, http.MethodPatch:
return true
default:
return false
}
}
func (ib *nativeInbound) xhttpHostAllowed(reqHost string) bool {
want := strings.TrimSpace(ib.xhttpHost)
if want == "" {
return true
}
for _, h := range strings.Split(want, ",") {
h = strings.TrimSpace(h)
if h == "" {
continue
}
if strings.EqualFold(reqHost, h) {
return true
}
reqBare := stripHostPort(reqHost)
wantBare := stripHostPort(h)
if strings.EqualFold(reqBare, wantBare) {
return true
}
}
return false
}
func stripHostPort(h string) string {
h = strings.TrimSpace(h)
if h == "" {
return ""
}
if strings.HasPrefix(h, "[") {
if end := strings.Index(h, "]"); end >= 0 {
return strings.Trim(h[1:end], "[]")
}
}
if host, _, err := net.SplitHostPort(h); err == nil {
return strings.Trim(host, "[]")
}
if i := strings.LastIndex(h, ":"); i > -1 && strings.Count(h, ":") == 1 {
return h[:i]
}
return strings.Trim(h, "[]")
}
func (ib *nativeInbound) matchXHTTPPath(reqPath string) (base string, ok bool) {
base = ib.path
if base == "" {
base = "/"
}
base = normalizeXHTTPPath(base)
if strings.HasPrefix(reqPath, base) {
return base, true
}
trimmed := strings.TrimSuffix(base, "/")
if trimmed == "" {
trimmed = "/"
}
if reqPath == trimmed {
return base, true
}
return base, false
}
func (ib *nativeInbound) writeXHTTPCommonHeaders(w http.ResponseWriter, r *http.Request) {
origin := r.Header.Get("Origin")
if origin == "" {
w.Header().Set("Access-Control-Allow-Origin", "*")
} else {
w.Header().Set("Access-Control-Allow-Origin", origin)
}
if r.Method == http.MethodOptions {
if m := r.Header.Get("Access-Control-Request-Method"); m != "" {
w.Header().Set("Access-Control-Allow-Methods", m)
} else {
w.Header().Set("Access-Control-Allow-Methods", "*")
}
if h := r.Header.Get("Access-Control-Request-Headers"); h != "" {
w.Header().Set("Access-Control-Allow-Headers", h)
} else {
w.Header().Set("Access-Control-Allow-Headers", "*")
}
}
}
func (ib *nativeInbound) extractXHTTPMeta(r *http.Request, base string) (sessionID, seqStr string) {
sessionPlacement := firstNonEmpty(ib.xhttpSessionPlacement, xhttpPlacementPath)
seqPlacement := firstNonEmpty(ib.xhttpSeqPlacement, xhttpPlacementPath)
sessionKey := firstNonEmpty(ib.xhttpSessionKey, defaultXHTTPMetaKey(sessionPlacement, true))
seqKey := firstNonEmpty(ib.xhttpSeqKey, defaultXHTTPMetaKey(seqPlacement, false))
var parts []string
pathPart := 0
if sessionPlacement == xhttpPlacementPath || seqPlacement == xhttpPlacementPath {
if strings.HasPrefix(r.URL.Path, base) {
parts = strings.Split(r.URL.Path[len(base):], "/")
}
}
if sessionPlacement == xhttpPlacementPath {
if len(parts) > pathPart {
sessionID = parts[pathPart]
pathPart++
}
} else {
sessionID = extractXHTTPValue(r, sessionPlacement, sessionKey)
}
if seqPlacement == xhttpPlacementPath {
if len(parts) > pathPart {
seqStr = parts[pathPart]
}
} else {
seqStr = extractXHTTPValue(r, seqPlacement, seqKey)
}
return sessionID, seqStr
}
func defaultXHTTPMetaKey(placement string, session bool) string {
switch placement {
case xhttpPlacementHeader:
if session {
return "X-Session"
}
return "X-Seq"
case xhttpPlacementCookie, xhttpPlacementQuery:
if session {
return "x_session"
}
return "x_seq"
default:
return ""
}
}
func extractXHTTPValue(r *http.Request, placement, key string) string {
if key == "" {
return ""
}
switch placement {
case xhttpPlacementQuery:
return r.URL.Query().Get(key)
case xhttpPlacementHeader:
return r.Header.Get(key)
case xhttpPlacementCookie:
if c, err := r.Cookie(key); err == nil {
return c.Value
}
}
return ""
}
func (ib *nativeInbound) upsertXHTTPSession(w http.ResponseWriter, id string) *nativeXHTTPSession {
ib.xhttpMu.Lock()
defer ib.xhttpMu.Unlock()
if ib.xhttpSessions == nil {
ib.xhttpSessions = make(map[string]*nativeXHTTPSession)
}
if s := ib.xhttpSessions[id]; s != nil {
s.touch()
return s
}
releaseSlot, ok := acquireNativeXHTTPSession()
if !ok {
logNativeLimitRejection("simultaneous XHTTP sessions", &nativeXHTTPRejected, nativeXHTTPSessionLimit())
w.Header().Set("Retry-After", "1")
http.Error(w, "xhttp session capacity temporarily busy", http.StatusServiceUnavailable)
return nil
}
s := &nativeXHTTPSession{
id: id,
queue: newNativeXHTTPUploadQueue(ib.xhttpMaxBufferedPosts, nativeXHTTPMaxBufferedSessionBytes),
done: make(chan struct{}),
connectedCh: make(chan struct{}),
lastSeen: time.Now(),
releaseSlot: releaseSlot,
}
ib.xhttpSessions[id] = s
xrayTracef("native xray: xhttp session created inbound=%q session=%q active=%d", ib.tag, id, len(ib.xhttpSessions))
xrayGo(fmt.Sprintf("native xray XHTTP session reaper inbound=%q session=%q", ib.tag, id), func() { ib.reapUnconnectedXHTTPSession(id, s) })
return s
}
func (ib *nativeInbound) xhttpMaxActiveSessions() int {
return nativeXHTTPSessionLimit()
}
func (ib *nativeInbound) reapUnconnectedXHTTPSession(id string, s *nativeXHTTPSession) {
timer := time.NewTimer(30 * time.Second)
defer timer.Stop()
select {
case <-timer.C:
s.mu.Lock()
connected := s.connected
s.mu.Unlock()
if !connected {
ib.deleteXHTTPSession(id, s)
s.close()
}
case <-s.connectedCh:
case <-s.done:
}
}
func (ib *nativeInbound) deleteXHTTPSession(id string, s *nativeXHTTPSession) {
ib.xhttpMu.Lock()
defer ib.xhttpMu.Unlock()
if ib.xhttpSessions[id] == s {
delete(ib.xhttpSessions, id)
}
}
func (ib *nativeInbound) closeAllXHTTPSessions() {
ib.xhttpMu.Lock()
sessions := make([]*nativeXHTTPSession, 0, len(ib.xhttpSessions))
for id, session := range ib.xhttpSessions {
delete(ib.xhttpSessions, id)
sessions = append(sessions, session)
}
ib.xhttpMu.Unlock()
for _, session := range sessions {
session.close()
}
}
func (ib *nativeInbound) handleXHTTPStreamUpload(w http.ResponseWriter, r *http.Request, sess *nativeXHTTPSession) {
sess.touch()
xrayTracef("native xray: xhttp stream-up inbound=%q session=%q len=%d remote=%s", ib.tag, sess.id, r.ContentLength, r.RemoteAddr)
if ib.xhttpMode != "" && ib.xhttpMode != "auto" && ib.xhttpMode != "stream-up" && ib.xhttpMode != "stream-down" {
http.Error(w, "xhttp stream-up mode is not allowed", http.StatusBadRequest)
return
}
if err := sess.queue.push(r.Context(), nativeXHTTPPacket{Reader: r.Body}, nil); err != nil {
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
return
}
http.Error(w, err.Error(), http.StatusConflict)
return
}
w.Header().Set("X-Accel-Buffering", "no")
w.Header().Set("Cache-Control", "no-store")
w.WriteHeader(http.StatusOK)
flushHTTP(w)
select {
case <-r.Context().Done():
case <-sess.done:
}
}
func (ib *nativeInbound) handleXHTTPPacketUpload(w http.ResponseWriter, r *http.Request, sess *nativeXHTTPSession, seqStr string) {
sess.touch()
if ib.xhttpMode != "" && ib.xhttpMode != "auto" && ib.xhttpMode != "packet-up" && ib.xhttpMode != "stream-down" {
http.Error(w, "xhttp packet-up mode is not allowed", http.StatusBadRequest)
return
}
seq, err := strconv.ParseUint(seqStr, 10, 64)
if err != nil {
http.Error(w, "bad xhttp sequence", http.StatusBadRequest)
return
}
// Reserve the expected payload rather than the configured maximum. Normal
// XHTTP body uploads have a Content-Length, so small packets no longer each
// consume a full 1 MB reservation. Unknown/chunked or metadata-carried uploads
// still reserve the maximum before decoding to preserve the hard memory bound.
memory, err := acquireNativeXHTTPMemoryContext(r.Context(), ib.xhttpUploadReservationBytes(r))
if err != nil {
// If the client/CDN canceled while waiting for backpressure, there is no
// useful HTTP error to send. Returning also releases every reservation.
return
}
defer memory.release()
payload, err := ib.readXHTTPPayload(r)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
memory.shrink(nativeXHTTPAccountedPacketBytes(int64(len(payload))))
xrayTracef("native xray: xhttp packet-up inbound=%q session=%q seq=%d payload=%d remote=%s", ib.tag, sess.id, seq, len(payload), r.RemoteAddr)
if err := sess.queue.push(r.Context(), nativeXHTTPPacket{Payload: payload, Seq: seq}, memory); err != nil {
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
return
}
if errors.Is(err, io.ErrClosedPipe) {
// A packet can race the stream-down request closing. Acknowledge the late
// upload instead of leaking an origin 500/502 into the reconnect path.
w.WriteHeader(http.StatusOK)
return
}
if errors.Is(err, errNativeXHTTPUploadBufferFull) {
http.Error(w, err.Error(), http.StatusRequestEntityTooLarge)
return
}
xrayTracef("native xray: xhttp packet-up push failed inbound=%q session=%q seq=%d: %v", ib.tag, sess.id, seq, err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if len(payload) == 0 {
w.Header().Set("Cache-Control", "no-store")
}
w.WriteHeader(http.StatusOK)
}
func (ib *nativeInbound) readXHTTPPayload(r *http.Request) ([]byte, error) {
placement := firstNonEmpty(ib.xhttpUplinkDataPlacement, xhttpPlacementBody)
key := firstNonEmpty(ib.xhttpUplinkDataKey, "X-Data")
var headerPayload, cookiePayload, bodyPayload []byte
var err error
if placement == xhttpPlacementAuto || placement == xhttpPlacementHeader {
headerPayload, err = readXHTTPHeaderPayload(r, key)
if err != nil {
return nil, err
}
}
if placement == xhttpPlacementAuto || placement == xhttpPlacementCookie {
cookiePayload, err = readXHTTPCookiePayload(r, key)
if err != nil {
return nil, err
}
}
if placement == xhttpPlacementAuto || placement == xhttpPlacementBody {
bodyPayload, err = ib.readXHTTPBodyPayload(r)
if err != nil {
return nil, err
}
}
var payload []byte
switch placement {
case xhttpPlacementHeader:
payload = headerPayload
case xhttpPlacementCookie:
payload = cookiePayload
case xhttpPlacementBody:
payload = bodyPayload
case xhttpPlacementAuto:
payload = append(payload, headerPayload...)
payload = append(payload, cookiePayload...)
payload = append(payload, bodyPayload...)
default:
return nil, fmt.Errorf("unsupported xhttp uplink data placement %q", placement)
}
if int64(len(payload)) > ib.xhttpMaxPostBytes() {
return nil, fmt.Errorf("xhttp upload too large")
}
return payload, nil
}
func readXHTTPHeaderPayload(r *http.Request, key string) ([]byte, error) {
chunks := make([]string, 0, 4)
for i := 0; ; i++ {
chunk := r.Header.Get(fmt.Sprintf("%s-%d", key, i))
if chunk == "" {
break
}
chunks = append(chunks, chunk)
}
if len(chunks) == 0 {
return nil, nil
}
return base64.RawURLEncoding.DecodeString(strings.Join(chunks, ""))
}
func readXHTTPCookiePayload(r *http.Request, key string) ([]byte, error) {
chunks := make([]string, 0, 4)
for i := 0; ; i++ {
cookieName := fmt.Sprintf("%s_%d", key, i)
c, err := r.Cookie(cookieName)
if err != nil {
break
}
chunks = append(chunks, c.Value)
}
if len(chunks) == 0 {
return nil, nil
}
return base64.RawURLEncoding.DecodeString(strings.Join(chunks, ""))
}
func (ib *nativeInbound) readXHTTPBodyPayload(r *http.Request) ([]byte, error) {
maxBytes := ib.xhttpMaxPostBytes()
if r.ContentLength > maxBytes {
return nil, fmt.Errorf("xhttp upload too large")
}
payload, err := io.ReadAll(io.LimitReader(r.Body, maxBytes+1))
if err != nil {
return nil, err
}
if int64(len(payload)) > maxBytes {
return nil, fmt.Errorf("xhttp upload too large")
}
return payload, nil
}
func (ib *nativeInbound) xhttpMaxPostBytes() int64 {
if ib.xhttpMaxEachPostBytes > 0 {
if ib.xhttpMaxEachPostBytes > nativeXHTTPHardMaxPostBytes {
return nativeXHTTPHardMaxPostBytes
}
return ib.xhttpMaxEachPostBytes
}
return 1_000_000
}
// xhttpUploadReservationBytes returns a safe pre-read reservation. Body-mode
// clients normally send Content-Length, which lets thousands of small packets
// share the global budget. Header/cookie/auto and chunked bodies reserve the
// configured maximum because their decoded size is not known until parsed.
func (ib *nativeInbound) xhttpUploadReservationBytes(r *http.Request) int64 {
maxBytes := ib.xhttpMaxPostBytes()
placement := firstNonEmpty(ib.xhttpUplinkDataPlacement, xhttpPlacementBody)
if placement == xhttpPlacementBody && r.ContentLength >= 0 {
if r.ContentLength > maxBytes {
return nativeXHTTPAccountedPacketBytes(maxBytes)
}
return nativeXHTTPAccountedPacketBytes(r.ContentLength)
}
return nativeXHTTPAccountedPacketBytes(maxBytes)
}
func nativeXHTTPAccountedPacketBytes(payloadBytes int64) int64 {
if payloadBytes < nativeXHTTPMinPacketAccountingBytes {
return nativeXHTTPMinPacketAccountingBytes
}
return payloadBytes
}
func (ib *nativeInbound) handleXHTTPStreamOne(w http.ResponseWriter, r *http.Request) {
defer xrayRecover(fmt.Sprintf("native xray XHTTP stream-one inbound=%q remote=%s", ib.tag, r.RemoteAddr))
xrayTracef("native xray: xhttp stream-one inbound=%q len=%d remote=%s", ib.tag, r.ContentLength, r.RemoteAddr)
w.Header().Set("X-Accel-Buffering", "no")
w.Header().Set("Cache-Control", "no-store")
if !ib.xhttpNoSSEHeader {
w.Header().Set("Content-Type", "text/event-stream")
}
w.WriteHeader(http.StatusOK)
flushHTTP(w)
remote := remoteAddrFromHTTPRequest(r)
resp := newNativeXHTTPResponseWriter(w)
xc := &nativeXHTTPConn{
reader: r.Body,
writer: resp,
remote: remote,
local: dummyLocalAddr(r),
onClose: func() {
resp.close()
_ = r.Body.Close()
},
}
ib.dispatchXHTTPConn(xc, remote)
_ = xc.Close()
}
func (ib *nativeInbound) handleXHTTPDownload(w http.ResponseWriter, r *http.Request, sess *nativeXHTTPSession, sessionID string) {
sess.touch()
defer xrayRecover(fmt.Sprintf("native xray XHTTP download inbound=%q session=%q remote=%s", ib.tag, sessionID, r.RemoteAddr))
xrayTracef("native xray: xhttp stream-down inbound=%q session=%q proto=%s remote=%s", ib.tag, sessionID, r.Proto, r.RemoteAddr)
sess.markConnected()
defer ib.deleteXHTTPSession(sessionID, sess)
w.Header().Set("X-Accel-Buffering", "no")
w.Header().Set("Cache-Control", "no-store")
if !ib.xhttpNoSSEHeader {
w.Header().Set("Content-Type", "text/event-stream")
}
w.WriteHeader(http.StatusOK)
flushHTTP(w)
remote := remoteAddrFromHTTPRequest(r)
var reader io.Reader = sess.queue
resp := newNativeXHTTPResponseWriter(w)
xc := &nativeXHTTPConn{
reader: reader,
writer: resp,
remote: remote,
local: dummyLocalAddr(r),
onActivity: sess.touch,
}
xc.onClose = func() {
resp.close()
sess.close()
}
// When the download GET is cancelled (client gone, or a CDN closes the
// origin stream after its own idle timeout) the HTTP request context fires.
// Closing xc unblocks the tunnel's uplink reader (via sess.close -> queue
// close) and closes the backend, so handleXHTTPDownload returns and its
// deferred deleteXHTTPSession runs. Without this watch an idle tunnel whose
// client vanished silently would never be torn down. The goroutine exits on
// sess.done once the session closes for any reason.
go func() {
select {
case <-r.Context().Done():
// The stream-down HTTP request is the lifetime owner of an XHTTP
// session. Log the actual transport cancellation so a CDN/proxy
// timeout can be distinguished from a server idle policy.
xrayTracef("native xray: xhttp stream-down ended inbound=%q session=%q remote=%s err=%v",
ib.tag, sessionID, r.RemoteAddr, r.Context().Err())
_ = xc.Close()
case <-sess.done:
}
}()
ib.dispatchXHTTPConn(xc, remote)
_ = xc.Close()
}
func (ib *nativeInbound) dispatchXHTTPConn(xc net.Conn, remote net.Addr) {
xrayTracef("native xray: xhttp dispatch inbound=%q protocol=%s remote=%s", ib.tag, ib.protocol, remote)
switch ib.protocol {
case "vless":
ib.handleVLESS(xc, remote)
case "vmess":
ib.handleVMess(xc, remote)
case "ssh":
// XHTTP->SSH tunnel: the decoded stream is a raw SSH transport. Hand it to
// the same SSH handler the TLS/DNSTT listeners use so tunneled clients
// authenticate with ordinary SSH accounts. getSSHConfig() is the live,
// hot-reloadable config; it can be nil only before the SSH server is set up.
cfg := getSSHConfig()
if cfg == nil {
xrayLogf("native xray: inbound %q XHTTP->SSH has no SSH config available yet", ib.tag)
return
}
handleConn(xc, cfg)
default:
xrayLogf("native xray: inbound %q XHTTP protocol %q not supported", ib.tag, ib.protocol)
}
}
func remoteAddrFromHTTPRequest(r *http.Request) net.Addr {
addr, err := net.ResolveTCPAddr("tcp", r.RemoteAddr)
if err == nil {
return addr
}
return &net.TCPAddr{IP: net.IPv4zero, Port: 0}
}
func dummyLocalAddr(r *http.Request) net.Addr {
if r.TLS != nil && r.Host != "" {
return &net.TCPAddr{IP: net.IPv4zero, Port: 443}
}
return &net.TCPAddr{IP: net.IPv4zero, Port: 80}
}
func flushHTTP(w http.ResponseWriter) {
if f, ok := w.(http.Flusher); ok {
f.Flush()
}
}
type nativeXHTTPSession struct {
id string
queue *nativeXHTTPUploadQueue
done chan struct{}
closeOnce sync.Once
connectedCh chan struct{} // closed once the download GET attaches
connectOnce sync.Once
mu sync.Mutex
connected bool
lastSeen time.Time
releaseSlot func()
}
func (s *nativeXHTTPSession) touch() {
s.mu.Lock()
s.lastSeen = time.Now()
s.mu.Unlock()
}
func (s *nativeXHTTPSession) markConnected() {
s.mu.Lock()
s.connected = true
s.lastSeen = time.Now()
s.mu.Unlock()
s.connectOnce.Do(func() { close(s.connectedCh) })
}
func (s *nativeXHTTPSession) close() {
s.closeOnce.Do(func() {
close(s.done)
s.queue.close()
if s.releaseSlot != nil {
s.releaseSlot()
}
})
}
type nativeXHTTPConn struct {
reader io.Reader
writer io.Writer
remote net.Addr
local net.Addr
deadlineMu sync.Mutex
readDeadline time.Time
closeOnce sync.Once
onClose func()
// onActivity, when set, is called after any successful read or write so the
// owning session's lastSeen reflects real bidirectional traffic (not just
// HTTP request arrivals). The idle sweeper relies on this to avoid reaping a
// tunnel that is actively streaming in only one direction.
onActivity func()
}
func (c *nativeXHTTPConn) Read(p []byte) (int, error) {
if dr, ok := c.reader.(interface{ SetReadDeadline(time.Time) error }); ok {
c.deadlineMu.Lock()
d := c.readDeadline
c.deadlineMu.Unlock()
_ = dr.SetReadDeadline(d)
}
n, err := c.reader.Read(p)
if n > 0 && c.onActivity != nil {
c.onActivity()
}
return n, err
}
func (c *nativeXHTTPConn) Write(p []byte) (int, error) {
n, err := c.writer.Write(p)
if n > 0 && c.onActivity != nil {
c.onActivity()
}
return n, err
}
func (c *nativeXHTTPConn) Close() error {
c.closeOnce.Do(func() {
if c.onClose != nil {
c.onClose()
}
})
return nil
}
func (c *nativeXHTTPConn) LocalAddr() net.Addr { return c.local }
func (c *nativeXHTTPConn) RemoteAddr() net.Addr { return c.remote }
func (c *nativeXHTTPConn) SetDeadline(t time.Time) error {
_ = c.SetReadDeadline(t)
return c.SetWriteDeadline(t)
}
func (c *nativeXHTTPConn) SetReadDeadline(t time.Time) error {
c.deadlineMu.Lock()
c.readDeadline = t
c.deadlineMu.Unlock()
if dr, ok := c.reader.(interface{ SetReadDeadline(time.Time) error }); ok {
return dr.SetReadDeadline(t)
}
return nil
}
func (c *nativeXHTTPConn) SetWriteDeadline(t time.Time) error {
if dw, ok := c.writer.(interface{ SetWriteDeadline(time.Time) error }); ok {
return dw.SetWriteDeadline(t)
}
return nil
}
type nativeXHTTPResponseWriter struct {
writeMu sync.Mutex
stateMu sync.Mutex
w http.ResponseWriter
closed bool
deadline time.Time
}
func newNativeXHTTPResponseWriter(w http.ResponseWriter) *nativeXHTTPResponseWriter {
return &nativeXHTTPResponseWriter{w: w}
}
func (w *nativeXHTTPResponseWriter) Write(p []byte) (int, error) {
w.writeMu.Lock()
defer w.writeMu.Unlock()
w.stateMu.Lock()
closed := w.closed
deadline := w.deadline
w.stateMu.Unlock()
if closed {
return 0, io.ErrClosedPipe
}
safetyDeadline := time.Now().Add(nativeXHTTPWriteTimeout())
if deadline.IsZero() || deadline.After(safetyDeadline) {
deadline = safetyDeadline
}
controller := http.NewResponseController(w.w)
if err := controller.SetWriteDeadline(deadline); err != nil && !errors.Is(err, http.ErrNotSupported) {
return 0, err
}
n, err := w.w.Write(p)
if err == nil {
if flushErr := controller.Flush(); flushErr != nil && !errors.Is(flushErr, http.ErrNotSupported) {
err = flushErr
}
}
return n, err
}
func (w *nativeXHTTPResponseWriter) close() {
// Do not wait for writeMu: Close is commonly called by the request-context
// watcher specifically because a CDN write is stalled. Mark the writer closed
// and force the active net/http write deadline to expire so Write returns.
w.stateMu.Lock()
w.closed = true
w.stateMu.Unlock()
_ = http.NewResponseController(w.w).SetWriteDeadline(time.Now())
}
func (w *nativeXHTTPResponseWriter) SetWriteDeadline(t time.Time) error {
w.stateMu.Lock()
w.deadline = t
w.stateMu.Unlock()
err := http.NewResponseController(w.w).SetWriteDeadline(t)
if errors.Is(err, http.ErrNotSupported) {
return nil
}
return err
}
// nativeXHTTPMemoryLease reserves from a process-wide byte budget before a
// packet-up handler allocates its payload. The same lease is transferred to the
// session queue, so active request bodies and queued reassembly data share one
// hard ceiling instead of each having an independent amplification window.
type nativeXHTTPMemoryLease struct {
bytes int64
}
func acquireNativeXHTTPMemory(n int64) (*nativeXHTTPMemoryLease, bool) {
if n <= 0 {
return &nativeXHTTPMemoryLease{}, true
}
nativeXHTTPMemoryWait.Lock()
defer nativeXHTTPMemoryWait.Unlock()
current := nativeXHTTPBufferedBytes.Load()
if nativeXHTTPMemoryWait.queued != 0 || current > nativeXHTTPMaxBufferedGlobalBytes-n {
logNativeLimitRejection("XHTTP buffered upload bytes", &nativeXHTTPBufferRejected, nativeXHTTPMaxBufferedGlobalBytes)
return nil, false
}
nativeXHTTPBufferedBytes.Store(current + n)
return &nativeXHTTPMemoryLease{bytes: n}, true
}
// acquireNativeXHTTPMemoryContext applies process-wide memory backpressure.
// Unlike the old fail-fast admission path, a legitimate tunnel burst waits for
// queued bytes to be consumed and remains cancelable if its HTTP request ends.
func acquireNativeXHTTPMemoryContext(ctx context.Context, n int64) (*nativeXHTTPMemoryLease, error) {
if n <= 0 {
return &nativeXHTTPMemoryLease{}, nil
}
if n > nativeXHTTPMaxBufferedGlobalBytes {
return nil, errNativeXHTTPUploadBufferFull
}
waiter := &nativeXHTTPMemoryWaiter{bytes: n, ready: make(chan struct{})}
nativeXHTTPMemoryWait.Lock()
current := nativeXHTTPBufferedBytes.Load()
if nativeXHTTPMemoryWait.queued == 0 && current <= nativeXHTTPMaxBufferedGlobalBytes-n {
nativeXHTTPBufferedBytes.Store(current + n)
nativeXHTTPMemoryWait.Unlock()
return &nativeXHTTPMemoryLease{bytes: n}, nil
}
nativeXHTTPMemoryWait.waiters = append(nativeXHTTPMemoryWait.waiters, waiter)
nativeXHTTPMemoryWait.queued++
nativeXHTTPMemoryWait.Unlock()
select {
case <-waiter.ready:
return &nativeXHTTPMemoryLease{bytes: n}, nil
case <-ctx.Done():
nativeXHTTPMemoryWait.Lock()
if waiter.granted {
current := nativeXHTTPBufferedBytes.Load() - n
if current < 0 {
current = 0
}
nativeXHTTPBufferedBytes.Store(current)
} else {
for i := nativeXHTTPMemoryWait.head; i < len(nativeXHTTPMemoryWait.waiters); i++ {
candidate := nativeXHTTPMemoryWait.waiters[i]
if candidate == waiter {
nativeXHTTPMemoryWait.waiters[i] = nil
nativeXHTTPMemoryWait.queued--
break
}
}
}
grantNativeXHTTPMemoryWaitersLocked()
nativeXHTTPMemoryWait.Unlock()
return nil, ctx.Err()
}
}
func releaseNativeXHTTPMemory(n int64) {
if n <= 0 {
return
}
nativeXHTTPMemoryWait.Lock()
current := nativeXHTTPBufferedBytes.Load()
next := current - n
if next < 0 {
next = 0
}
nativeXHTTPBufferedBytes.Store(next)
grantNativeXHTTPMemoryWaitersLocked()
nativeXHTTPMemoryWait.Unlock()
}
// grantNativeXHTTPMemoryWaitersLocked wakes only the FIFO waiters whose exact
// reservations now fit. The former broadcast channel woke every blocked HTTP
// handler after every tiny release, creating a thundering herd and sustained
// multi-core CPU usage while the 128 MB budget was full.
func grantNativeXHTTPMemoryWaitersLocked() {
for nativeXHTTPMemoryWait.queued > 0 {
for nativeXHTTPMemoryWait.head < len(nativeXHTTPMemoryWait.waiters) &&
nativeXHTTPMemoryWait.waiters[nativeXHTTPMemoryWait.head] == nil {
nativeXHTTPMemoryWait.head++
}
if nativeXHTTPMemoryWait.head >= len(nativeXHTTPMemoryWait.waiters) {
nativeXHTTPMemoryWait.waiters = nil
nativeXHTTPMemoryWait.head = 0
nativeXHTTPMemoryWait.queued = 0
return
}
waiter := nativeXHTTPMemoryWait.waiters[nativeXHTTPMemoryWait.head]
current := nativeXHTTPBufferedBytes.Load()
if current > nativeXHTTPMaxBufferedGlobalBytes-waiter.bytes {
compactNativeXHTTPMemoryWaitersLocked()
return
}
nativeXHTTPMemoryWait.waiters[nativeXHTTPMemoryWait.head] = nil
nativeXHTTPMemoryWait.head++
nativeXHTTPMemoryWait.queued--
nativeXHTTPBufferedBytes.Store(current + waiter.bytes)
waiter.granted = true
close(waiter.ready)
}
compactNativeXHTTPMemoryWaitersLocked()
}
func compactNativeXHTTPMemoryWaitersLocked() {
head := nativeXHTTPMemoryWait.head
if head == 0 {
return
}
if nativeXHTTPMemoryWait.queued == 0 {
nativeXHTTPMemoryWait.waiters = nil
nativeXHTTPMemoryWait.head = 0
return
}
if head < 1024 && head*2 < len(nativeXHTTPMemoryWait.waiters) {
return
}
remaining := copy(nativeXHTTPMemoryWait.waiters, nativeXHTTPMemoryWait.waiters[head:])
for i := remaining; i < len(nativeXHTTPMemoryWait.waiters); i++ {
nativeXHTTPMemoryWait.waiters[i] = nil
}
nativeXHTTPMemoryWait.waiters = nativeXHTTPMemoryWait.waiters[:remaining]
nativeXHTTPMemoryWait.head = 0
}
func (l *nativeXHTTPMemoryLease) shrink(n int64) {
if l == nil {
return
}
if n < 0 {
n = 0
}
if n >= l.bytes {
return
}
release := l.bytes - n
l.bytes = n
releaseNativeXHTTPMemory(release)
}
func (l *nativeXHTTPMemoryLease) release() {
if l == nil || l.bytes <= 0 {
return
}
n := l.bytes
l.bytes = 0
releaseNativeXHTTPMemory(n)
}
type nativeXHTTPPacket struct {
Reader io.ReadCloser
Payload []byte
Seq uint64
accountedBytes int64
}
type nativeXHTTPUploadQueue struct {
pushedPackets chan nativeXHTTPPacket
maxBytes int64
// readMu serializes the single decoded stream reader with close-time queue
// cleanup. pushWG lets close wait until every producer that started before
// closedFlag was set has either transferred or released its memory lease.
readMu sync.Mutex
pushWG sync.WaitGroup
mu sync.Mutex
reader io.ReadCloser
readerQueued bool
heap nativeXHTTPHeap
nextSeq uint64
readDeadline time.Time
bufferedBytes int64
closedFlag bool
spaceChanged chan struct{}
closed chan struct{}
closeOnce sync.Once
}
func newNativeXHTTPUploadQueue(maxPackets int, maxBytes int64) *nativeXHTTPUploadQueue {
if maxPackets <= 0 {
maxPackets = defaultNativeXHTTPBufferedPosts
}
if maxPackets > nativeXHTTPMaxBufferedPosts {
maxPackets = nativeXHTTPMaxBufferedPosts
}
if maxBytes <= 0 || maxBytes > nativeXHTTPMaxBufferedSessionBytes {
maxBytes = nativeXHTTPMaxBufferedSessionBytes
}
return &nativeXHTTPUploadQueue{
pushedPackets: make(chan nativeXHTTPPacket, maxPackets),
maxBytes: maxBytes,
closed: make(chan struct{}),
spaceChanged: make(chan struct{}),
}
}
func (q *nativeXHTTPUploadQueue) beginPush() bool {
q.mu.Lock()
defer q.mu.Unlock()
if q.closedFlag {
return false
}
q.pushWG.Add(1)
return true
}
func (q *nativeXHTTPUploadQueue) adoptPayloadMemory(ctx context.Context, memory *nativeXHTTPMemoryLease, n int64) error {
if n <= 0 {
return nil
}
if memory == nil || memory.bytes != n {
return errNativeXHTTPUploadBufferFull
}
if n > q.maxBytes {
return errNativeXHTTPUploadBufferFull
}
for {
q.mu.Lock()
if q.closedFlag {
q.mu.Unlock()
return io.ErrClosedPipe
}
if q.bufferedBytes <= q.maxBytes-n {
q.bufferedBytes += n
memory.bytes = 0
q.mu.Unlock()
return nil
}
changed := q.spaceChanged
q.mu.Unlock()
select {
case <-changed:
case <-q.closed:
return io.ErrClosedPipe
case <-ctx.Done():
return ctx.Err()
}
}
}
func (q *nativeXHTTPUploadQueue) releasePayloadMemory(n int64) {
if n <= 0 {
return
}
q.mu.Lock()
release := n
if release > q.bufferedBytes {
release = q.bufferedBytes
}
q.bufferedBytes -= release
close(q.spaceChanged)
q.spaceChanged = make(chan struct{})
q.mu.Unlock()
releaseNativeXHTTPMemory(release)
}
func (q *nativeXHTTPUploadQueue) push(ctx context.Context, p nativeXHTTPPacket, memory *nativeXHTTPMemoryLease) error {
if !q.beginPush() {
return io.ErrClosedPipe
}
defer q.pushWG.Done()
readerReserved := false
if p.Reader != nil {
q.mu.Lock()
if q.reader != nil || q.readerQueued || q.closedFlag {
q.mu.Unlock()
return errors.New("xhttp upload reader already exists")
}
q.readerQueued = true
readerReserved = true
q.mu.Unlock()
defer func() {
if readerReserved {
q.mu.Lock()
q.readerQueued = false
q.mu.Unlock()
}
}()
}
accountedBytes := int64(0)
if p.Reader == nil {
accountedBytes = nativeXHTTPAccountedPacketBytes(int64(len(p.Payload)))
}
if err := q.adoptPayloadMemory(ctx, memory, accountedBytes); err != nil {
return err
}
p.accountedBytes = accountedBytes
if accountedBytes > 0 {
defer func() {
if accountedBytes > 0 {
q.releasePayloadMemory(accountedBytes)
}
}()
}
select {
case q.pushedPackets <- p:
// Ownership has moved to the queue. close() waits for this producer and
// then drains/releases anything not consumed by the stream reader.
accountedBytes = 0
readerReserved = false
return nil
case <-q.closed:
return io.ErrClosedPipe
case <-ctx.Done():
return ctx.Err()
}
}
func (q *nativeXHTTPUploadQueue) close() {
q.closeOnce.Do(func() {
q.mu.Lock()
q.closedFlag = true
reader := q.reader
close(q.closed)
q.mu.Unlock()
if reader != nil {
_ = reader.Close()
}
q.pushWG.Wait()
q.readMu.Lock()
// No producers or readers can now change the queue. Drop references to
// buffered payloads promptly and return their exact byte reservation.
for {
select {
case p := <-q.pushedPackets:
if p.Reader != nil {
_ = p.Reader.Close()
}
p.Payload = nil
default:
goto drained
}
}
drained:
q.mu.Lock()
remaining := q.bufferedBytes
q.bufferedBytes = 0
for i := range q.heap {
q.heap[i].Payload = nil
}
q.heap = nil
q.mu.Unlock()
q.readMu.Unlock()
releaseNativeXHTTPMemory(remaining)
})
}
func (q *nativeXHTTPUploadQueue) SetReadDeadline(t time.Time) error {
q.mu.Lock()
q.readDeadline = t
q.mu.Unlock()
return nil
}
func (q *nativeXHTTPUploadQueue) recv() (nativeXHTTPPacket, error) {
q.mu.Lock()
d := q.readDeadline
q.mu.Unlock()
if d.IsZero() {
select {
case p := <-q.pushedPackets:
return p, nil
case <-q.closed:
return nativeXHTTPPacket{}, io.EOF
}
}
wait := time.Until(d)
if wait <= 0 {
return nativeXHTTPPacket{}, os.ErrDeadlineExceeded
}
timer := time.NewTimer(wait)
defer timer.Stop()
select {
case p := <-q.pushedPackets:
return p, nil
case <-q.closed:
return nativeXHTTPPacket{}, io.EOF
case <-timer.C:
return nativeXHTTPPacket{}, os.ErrDeadlineExceeded
}
}
func (q *nativeXHTTPUploadQueue) Read(b []byte) (int, error) {
q.readMu.Lock()
defer q.readMu.Unlock()
if reader := q.loadReader(); reader != nil {
return reader.Read(b)
}
for {
select {
case <-q.closed:
return 0, io.EOF
default:
}
if len(q.heap) == 0 {
p, err := q.recv()
if err != nil {
return 0, err
}
if p.Reader != nil {
if !q.setReader(p.Reader) {
_ = p.Reader.Close()
return 0, io.EOF
}
return p.Reader.Read(b)
}
select {
case <-q.closed:
q.releasePayloadMemory(p.accountedBytes)
return 0, io.EOF
default:
}
heap.Push(&q.heap, p)
}
for len(q.heap) > 0 {
packet := heap.Pop(&q.heap).(nativeXHTTPPacket)
if packet.Seq == q.nextSeq {
if len(packet.Payload) == 0 {
q.releasePayloadMemory(packet.accountedBytes)
q.nextSeq = packet.Seq + 1
continue
}
n := copy(b, packet.Payload)
if n < len(packet.Payload) {
q.releasePayloadMemory(int64(n))
packet.accountedBytes -= int64(n)
packet.Payload = packet.Payload[n:]
heap.Push(&q.heap, packet)
} else {
q.releasePayloadMemory(packet.accountedBytes)
q.nextSeq = packet.Seq + 1
}
return n, nil
}
if packet.Seq > q.nextSeq {
heap.Push(&q.heap, packet)
p, err := q.recv()
if err != nil {
return 0, err
}
if p.Reader != nil {
_ = p.Reader.Close()
return 0, errors.New("xhttp mixed stream-up and packet-up upload")
}
heap.Push(&q.heap, p)
continue
}
// A duplicate/late packet is discarded; release the bytes it owned.
q.releasePayloadMemory(packet.accountedBytes)
}
}
}
func (q *nativeXHTTPUploadQueue) loadReader() io.ReadCloser {
q.mu.Lock()
defer q.mu.Unlock()
return q.reader
}
func (q *nativeXHTTPUploadQueue) setReader(r io.ReadCloser) bool {
q.mu.Lock()
defer q.mu.Unlock()
if q.closedFlag {
return false
}
q.reader = r
return true
}
type nativeXHTTPHeap []nativeXHTTPPacket
func (h nativeXHTTPHeap) Len() int { return len(h) }
func (h nativeXHTTPHeap) Less(i, j int) bool { return h[i].Seq < h[j].Seq }
func (h nativeXHTTPHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] }
func (h *nativeXHTTPHeap) Push(x any) { *h = append(*h, x.(nativeXHTTPPacket)) }
func (h *nativeXHTTPHeap) Pop() any {
old := *h
n := len(old)
x := old[n-1]
*h = old[:n-1]
return x
}