github.com/sagernet/sing-box@v1.9.0-rc.20/transport/v2rayhttp/client.go (about)

     1  package v2rayhttp
     2  
     3  import (
     4  	"context"
     5  	"io"
     6  	"math/rand"
     7  	"net"
     8  	"net/http"
     9  	"net/url"
    10  	"strings"
    11  	"time"
    12  
    13  	"github.com/sagernet/sing-box/adapter"
    14  	"github.com/sagernet/sing-box/common/tls"
    15  	"github.com/sagernet/sing-box/option"
    16  	E "github.com/sagernet/sing/common/exceptions"
    17  	M "github.com/sagernet/sing/common/metadata"
    18  	N "github.com/sagernet/sing/common/network"
    19  	sHTTP "github.com/sagernet/sing/protocol/http"
    20  
    21  	"golang.org/x/net/http2"
    22  )
    23  
    24  var _ adapter.V2RayClientTransport = (*Client)(nil)
    25  
    26  type Client struct {
    27  	ctx        context.Context
    28  	dialer     N.Dialer
    29  	serverAddr M.Socksaddr
    30  	transport  http.RoundTripper
    31  	http2      bool
    32  	requestURL url.URL
    33  	host       []string
    34  	method     string
    35  	headers    http.Header
    36  }
    37  
    38  func NewClient(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, options option.V2RayHTTPOptions, tlsConfig tls.Config) (adapter.V2RayClientTransport, error) {
    39  	var transport http.RoundTripper
    40  	if tlsConfig == nil {
    41  		transport = &http.Transport{
    42  			DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
    43  				return dialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
    44  			},
    45  		}
    46  	} else {
    47  		if len(tlsConfig.NextProtos()) == 0 {
    48  			tlsConfig.SetNextProtos([]string{http2.NextProtoTLS})
    49  		}
    50  		transport = &http2.Transport{
    51  			ReadIdleTimeout: time.Duration(options.IdleTimeout),
    52  			PingTimeout:     time.Duration(options.PingTimeout),
    53  			DialTLSContext: func(ctx context.Context, network, addr string, cfg *tls.STDConfig) (net.Conn, error) {
    54  				conn, err := dialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
    55  				if err != nil {
    56  					return nil, err
    57  				}
    58  				return tls.ClientHandshake(ctx, conn, tlsConfig)
    59  			},
    60  		}
    61  	}
    62  	if options.Method == "" {
    63  		options.Method = http.MethodPut
    64  	}
    65  	var requestURL url.URL
    66  	if tlsConfig == nil {
    67  		requestURL.Scheme = "http"
    68  	} else {
    69  		requestURL.Scheme = "https"
    70  	}
    71  	requestURL.Host = serverAddr.String()
    72  	requestURL.Path = options.Path
    73  	err := sHTTP.URLSetPath(&requestURL, options.Path)
    74  	if err != nil {
    75  		return nil, E.Cause(err, "parse path")
    76  	}
    77  	if !strings.HasPrefix(requestURL.Path, "/") {
    78  		requestURL.Path = "/" + requestURL.Path
    79  	}
    80  	return &Client{
    81  		ctx:        ctx,
    82  		dialer:     dialer,
    83  		serverAddr: serverAddr,
    84  		requestURL: requestURL,
    85  		host:       options.Host,
    86  		method:     options.Method,
    87  		headers:    options.Headers.Build(),
    88  		transport:  transport,
    89  		http2:      tlsConfig != nil,
    90  	}, nil
    91  }
    92  
    93  func (c *Client) DialContext(ctx context.Context) (net.Conn, error) {
    94  	if !c.http2 {
    95  		return c.dialHTTP(ctx)
    96  	} else {
    97  		return c.dialHTTP2(ctx)
    98  	}
    99  }
   100  
   101  func (c *Client) dialHTTP(ctx context.Context) (net.Conn, error) {
   102  	conn, err := c.dialer.DialContext(ctx, N.NetworkTCP, c.serverAddr)
   103  	if err != nil {
   104  		return nil, err
   105  	}
   106  
   107  	request := &http.Request{
   108  		Method: c.method,
   109  		URL:    &c.requestURL,
   110  		Header: c.headers.Clone(),
   111  	}
   112  	switch hostLen := len(c.host); hostLen {
   113  	case 0:
   114  		request.Host = c.serverAddr.AddrString()
   115  	case 1:
   116  		request.Host = c.host[0]
   117  	default:
   118  		request.Host = c.host[rand.Intn(hostLen)]
   119  	}
   120  
   121  	return NewHTTP1Conn(conn, request), nil
   122  }
   123  
   124  func (c *Client) dialHTTP2(ctx context.Context) (net.Conn, error) {
   125  	pipeInReader, pipeInWriter := io.Pipe()
   126  	request := &http.Request{
   127  		Method: c.method,
   128  		Body:   pipeInReader,
   129  		URL:    &c.requestURL,
   130  		Header: c.headers.Clone(),
   131  	}
   132  	request = request.WithContext(ctx)
   133  	switch hostLen := len(c.host); hostLen {
   134  	case 0:
   135  		// https://github.com/v2fly/v2ray-core/blob/master/transport/internet/http/config.go#L13
   136  		request.Host = "www.example.com"
   137  	case 1:
   138  		request.Host = c.host[0]
   139  	default:
   140  		request.Host = c.host[rand.Intn(hostLen)]
   141  	}
   142  	conn := NewLateHTTPConn(pipeInWriter)
   143  	go func() {
   144  		response, err := c.transport.RoundTrip(request)
   145  		if err != nil {
   146  			conn.Setup(nil, err)
   147  		} else if response.StatusCode != 200 {
   148  			response.Body.Close()
   149  			conn.Setup(nil, E.New("unexpected status: ", response.Status))
   150  		} else {
   151  			conn.Setup(response.Body, nil)
   152  		}
   153  	}()
   154  	return conn, nil
   155  }
   156  
   157  func (c *Client) Close() error {
   158  	CloseIdleConnections(c.transport)
   159  	return nil
   160  }