package v2rayhttp import ( "bufio" "context" "crypto/tls" "io" "math/rand" "net" "net/http" "net/url" "strings" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/option" E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" ) var _ adapter.V2RayClientTransport = (*Client)(nil) type Client struct { ctx context.Context dialer N.Dialer serverAddr M.Socksaddr client *http.Client http2 bool url *url.URL host []string method string headers http.Header } func NewClient(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, options option.V2RayHTTPOptions, tlsConfig *tls.Config) adapter.V2RayClientTransport { client := &Client{ ctx: ctx, dialer: dialer, serverAddr: serverAddr, host: options.Host, method: options.Method, headers: make(http.Header), client: &http.Client{ Transport: &http.Transport{ DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { return dialer.DialContext(ctx, network, M.ParseSocksaddr(addr)) }, ForceAttemptHTTP2: true, TLSClientConfig: tlsConfig, }, }, http2: tlsConfig != nil, } if client.method == "" { client.method = "PUT" } var uri url.URL if tlsConfig == nil { uri.Scheme = "http" } else { uri.Scheme = "https" } uri.Host = serverAddr.String() uri.Path = options.Path if !strings.HasPrefix(uri.Path, "/") { uri.Path = "/" + uri.Path } for key, value := range options.Headers { client.headers.Set(key, value) } client.url = &uri return client } func (c *Client) DialContext(ctx context.Context) (net.Conn, error) { if !c.http2 { return c.dialHTTP(ctx) } else { return c.dialHTTP2(ctx) } } func (c *Client) dialHTTP(ctx context.Context) (net.Conn, error) { conn, err := c.dialer.DialContext(c.ctx, N.NetworkTCP, c.serverAddr) if err != nil { return nil, err } request := &http.Request{ Method: c.method, URL: c.url, ProtoMajor: 1, Proto: "HTTP/1.1", Header: c.headers.Clone(), } request = request.WithContext(ctx) switch hostLen := len(c.host); hostLen { case 0: case 1: request.Host = c.host[0] default: request.Host = c.host[rand.Intn(hostLen)] } err = request.Write(conn) if err != nil { return nil, err } reader := bufio.NewReader(conn) response, err := http.ReadResponse(reader, request) if err != nil { return nil, err } if response.StatusCode != 200 { return nil, E.New("unexpected status: ", response.Status) } return conn, nil } func (c *Client) dialHTTP2(ctx context.Context) (net.Conn, error) { pipeInReader, pipeInWriter := io.Pipe() request := &http.Request{ Method: c.method, Body: pipeInReader, URL: c.url, ProtoMajor: 2, Proto: "HTTP/2", Header: c.headers.Clone(), } request = request.WithContext(ctx) switch hostLen := len(c.host); hostLen { case 0: case 1: request.Host = c.host[0] default: request.Host = c.host[rand.Intn(hostLen)] } // Disable any compression method from server. request.Header.Set("Accept-Encoding", "identity") response, err := c.client.Do(request) // nolint: bodyclose if err != nil { pipeInWriter.Close() return nil, err } if response.StatusCode != 200 { return nil, E.New("unexpected status: ", response.StatusCode, " ", response.Status) } return &HTTPConn{ response.Body, pipeInWriter, }, nil }