mirror of
https://github.com/XTLS/Xray-core.git
synced 2026-10-20 00:04:26 +00:00
https://github.com/XTLS/Xray-core/pull/7170#issuecomment-6204133548
225 lines
6.1 KiB
Go
225 lines
6.1 KiB
Go
package xdrive
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
"net/http"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/xtls/xray-core/common/errors"
|
|
"github.com/xtls/xray-core/common/net"
|
|
"github.com/xtls/xray-core/transport/internet"
|
|
"github.com/xtls/xray-core/transport/internet/tls"
|
|
)
|
|
|
|
var errStalled = errors.New("the request made no progress")
|
|
|
|
type serviceTransport struct {
|
|
plain http.RoundTripper
|
|
secure http.RoundTripper
|
|
stall time.Duration
|
|
}
|
|
|
|
// RoundTrip ends a request that stands still for t.stall, with no answer that
|
|
// long after its body went out or no byte of either body, but not one that just
|
|
// runs long: a large object on a slow link takes minutes and is fine as long as
|
|
// its bytes move
|
|
func (t *serviceTransport) RoundTrip(r *http.Request) (*http.Response, error) {
|
|
next := t.plain
|
|
if r.URL.Scheme == "https" {
|
|
next = t.secure
|
|
}
|
|
|
|
ctx, cancel := context.WithCancelCause(r.Context())
|
|
timer := time.AfterFunc(t.stall, func() { cancel(errStalled) })
|
|
alive := func() { timer.Reset(t.stall) }
|
|
end := func() {
|
|
timer.Stop()
|
|
cancel(nil)
|
|
}
|
|
r = r.WithContext(ctx)
|
|
if r.Body != nil && r.Body != http.NoBody {
|
|
r.Body = &progressBody{ReadCloser: r.Body, alive: alive}
|
|
}
|
|
// a body sent again by the transport itself, after a GOAWAY or on a reused
|
|
// connection that failed, moves just the same
|
|
if getBody := r.GetBody; getBody != nil {
|
|
r.GetBody = func() (io.ReadCloser, error) {
|
|
body, err := getBody()
|
|
if err != nil || body == http.NoBody {
|
|
return body, err
|
|
}
|
|
return &progressBody{ReadCloser: body, alive: alive}, nil
|
|
}
|
|
}
|
|
|
|
resp, err := next.RoundTrip(r)
|
|
if err != nil {
|
|
err = stalled(ctx, err)
|
|
end()
|
|
return nil, err
|
|
}
|
|
alive()
|
|
resp.Body = &progressBody{ReadCloser: resp.Body, alive: alive, end: end, ctx: ctx}
|
|
return resp, nil
|
|
}
|
|
|
|
// stalled returns errStalled for an error that the stall gave, which http2 reports
|
|
// as a plain cancel
|
|
func stalled(ctx context.Context, err error) error {
|
|
if context.Cause(ctx) == errStalled {
|
|
return errStalled
|
|
}
|
|
return err
|
|
}
|
|
|
|
type progressBody struct {
|
|
io.ReadCloser
|
|
alive func()
|
|
end func()
|
|
// ctx is that of the request, for the body of its response
|
|
ctx context.Context
|
|
}
|
|
|
|
func (b *progressBody) Read(p []byte) (int, error) {
|
|
// http2 reads a request body up to 512 KiB at a time and sends it all before
|
|
// the next read, which on a slow link could look like no progress
|
|
n, err := b.ReadCloser.Read(p[:min(len(p), 16<<10)])
|
|
if n > 0 {
|
|
b.alive()
|
|
}
|
|
if err != nil && err != io.EOF && b.ctx != nil {
|
|
err = stalled(b.ctx, err)
|
|
}
|
|
return n, err
|
|
}
|
|
|
|
func (b *progressBody) Close() error {
|
|
err := b.ReadCloser.Close()
|
|
if b.end != nil {
|
|
b.end()
|
|
}
|
|
return err
|
|
}
|
|
|
|
// newServiceClient returns a client whose requests end after stall without
|
|
// progress, see serviceTransport.RoundTrip
|
|
func newServiceClient(streamSettings *internet.MemoryStreamConfig, stall time.Duration, maxConns int) *http.Client {
|
|
var (
|
|
tlsConfig *tls.Config
|
|
sockopt *internet.SocketConfig
|
|
fronting *net.Destination
|
|
)
|
|
if streamSettings != nil {
|
|
tlsConfig = tls.ConfigFromStreamSettings(streamSettings)
|
|
sockopt = streamSettings.SocketSettings
|
|
fronting = streamSettings.Destination
|
|
}
|
|
if tlsConfig == nil {
|
|
tlsConfig = &tls.Config{}
|
|
}
|
|
fingerprint := tls.GetFingerprint(tlsConfig.Fingerprint)
|
|
|
|
dial := func(ctx context.Context, addr string) (net.Conn, net.Destination, error) {
|
|
host, err := net.ParseDestination("tcp:" + addr)
|
|
if err != nil {
|
|
return nil, host, errors.New("bad address: ", addr).Base(err)
|
|
}
|
|
|
|
target := host
|
|
if fronting != nil {
|
|
target.Address = fronting.Address
|
|
if fronting.Port != 0 {
|
|
target.Port = fronting.Port
|
|
}
|
|
}
|
|
|
|
var conn net.Conn
|
|
if streamSettings.FinalMask != nil {
|
|
conn, err = streamSettings.FinalMask.DialTCP(ctx, target)
|
|
} else {
|
|
conn, err = internet.DialSystem(ctx, target, sockopt)
|
|
}
|
|
if err != nil {
|
|
return nil, host, errors.New("failed to dial to dest").Base(err)
|
|
}
|
|
return conn, host, nil
|
|
}
|
|
|
|
dialPlain := func(ctx context.Context, network, addr string) (net.Conn, error) {
|
|
conn, _, err := dial(ctx, addr)
|
|
return conn, err
|
|
}
|
|
|
|
// http.Transport picks h2 or h1 by the ALPN in the ConnectionState of the conn
|
|
dialTLS := func(ctx context.Context, network, addr string) (net.Conn, error) {
|
|
conn, host, err := dial(ctx, addr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
gotlsConfig := tlsConfig.GetTLSConfig(tls.WithDestination(host))
|
|
var tlsConn net.Conn
|
|
if fingerprint == nil {
|
|
tlsConn = tls.Client(conn, gotlsConfig)
|
|
} else {
|
|
tlsConn = tls.UClient(conn, gotlsConfig, fingerprint)
|
|
}
|
|
// the dial outlives the request that started it, and a handshake that hangs
|
|
// would keep the one connection of the host to itself
|
|
ctx, cancel := context.WithTimeout(ctx, stall)
|
|
defer cancel()
|
|
if err := tlsConn.(tls.Interface).HandshakeContext(ctx); err != nil {
|
|
conn.Close()
|
|
return nil, err
|
|
}
|
|
return tlsConn, nil
|
|
}
|
|
|
|
secure := func(connsPerHost int) *http.Transport {
|
|
return &http.Transport{
|
|
DialTLSContext: dialTLS,
|
|
ForceAttemptHTTP2: true,
|
|
IdleConnTimeout: net.ConnIdleTimeout,
|
|
MaxIdleConns: maxConns,
|
|
MaxIdleConnsPerHost: maxConns,
|
|
MaxConnsPerHost: connsPerHost,
|
|
HTTP2: &http.HTTP2Config{SendPingTimeout: net.ChromeH2KeepAlivePeriod},
|
|
}
|
|
}
|
|
|
|
return &http.Client{
|
|
Transport: &serviceTransport{
|
|
plain: &http.Transport{
|
|
DialContext: dialPlain,
|
|
IdleConnTimeout: net.ConnIdleTimeout,
|
|
MaxIdleConns: maxConns,
|
|
MaxIdleConnsPerHost: maxConns,
|
|
MaxConnsPerHost: maxConns,
|
|
},
|
|
secure: &secureTransport{h2: secure(1), h1: secure(maxConns)},
|
|
stall: stall,
|
|
},
|
|
}
|
|
}
|
|
|
|
// secureTransport keeps a host to one connection, all that h2 needs, as without
|
|
// a limit parallel requests would each dial their own. A host that answers over
|
|
// h1 moves to a transport with more, since h1 carries one request at a time
|
|
type secureTransport struct {
|
|
h2, h1 http.RoundTripper
|
|
h1Hosts sync.Map
|
|
}
|
|
|
|
func (t *secureTransport) RoundTrip(r *http.Request) (*http.Response, error) {
|
|
if _, ok := t.h1Hosts.Load(r.URL.Host); ok {
|
|
return t.h1.RoundTrip(r)
|
|
}
|
|
resp, err := t.h2.RoundTrip(r)
|
|
if err == nil && resp.ProtoMajor == 1 {
|
|
t.h1Hosts.Store(r.URL.Host, true)
|
|
}
|
|
return resp, err
|
|
}
|