diff --git a/proxy/http/client.go b/proxy/http/client.go index 51dca746e..5dd2d7c07 100644 --- a/proxy/http/client.go +++ b/proxy/http/client.go @@ -3,10 +3,15 @@ package http import ( + "bufio" "context" "encoding/base64" "io" - "strings" + "net/http" + "net/url" + "sync" + + "golang.org/x/net/http2" "v2ray.com/core" "v2ray.com/core/common" @@ -20,6 +25,7 @@ import ( "v2ray.com/core/features/policy" "v2ray.com/core/transport" "v2ray.com/core/transport/internet" + "v2ray.com/core/transport/internet/tls" ) type Client struct { @@ -27,6 +33,16 @@ type Client struct { policyManager policy.Manager } +type h2Conn struct { + rawConn net.Conn + h2Conn *http2.ClientConn +} + +var ( + cachedH2Mutex sync.Mutex + cachedH2Conns map[net.Destination]h2Conn +) + // NewClient create a new http client based on the given config. func NewClient(ctx context.Context, config *ClientConfig) (*Client, error) { serverList := protocol.NewServerList() @@ -54,25 +70,26 @@ func (c *Client) Process(ctx context.Context, link *transport.Link, dialer inter if outbound == nil || !outbound.Target.IsValid() { return newError("target not specified.") } - destination := outbound.Target + target := outbound.Target - if destination.Network == net.Network_UDP { + if target.Network == net.Network_UDP { return newError("UDP is not supported by HTTP outbound") } - var server *protocol.ServerSpec + var user *protocol.MemoryUser var conn internet.Connection if err := retry.ExponentialBackoff(5, 100).On(func() error { - server = c.serverPicker.PickServer() + server := c.serverPicker.PickServer() dest := server.Destination() - rawConn, err := dialer.Dial(ctx, dest) - if err != nil { - return err - } - conn = rawConn + user = server.PickUser() + targetAddr := target.NetAddr() - return nil + netConn, err := setUpHttpTunnel(ctx, dest, targetAddr, user, dialer) + if netConn != nil { + conn = internet.Connection(netConn) + } + return err }); err != nil { return newError("failed to find an available destination").Base(err) } @@ -84,16 +101,10 @@ func (c *Client) Process(ctx context.Context, link *transport.Link, dialer inter }() p := c.policyManager.ForLevel(0) - - user := server.PickUser() if user != nil { p = c.policyManager.ForLevel(user.Level) } - if err := setUpHttpTunnel(conn, conn, &destination, user); err != nil { - return err - } - ctx, cancel := context.WithCancel(ctx) timer := signal.CancelAfterInactivity(ctx, cancel, p.Timeouts.ConnectionIdle) @@ -115,30 +126,164 @@ func (c *Client) Process(ctx context.Context, link *transport.Link, dialer inter } // setUpHttpTunnel will create a socket tunnel via HTTP CONNECT method -func setUpHttpTunnel(reader io.Reader, writer io.Writer, destination *net.Destination, user *protocol.MemoryUser) error { - var headers []string - destNetAddr := destination.NetAddr() - headers = append(headers, "CONNECT "+destNetAddr+" HTTP/1.1") - headers = append(headers, "Host: "+destNetAddr) +func setUpHttpTunnel(ctx context.Context, dest net.Destination, target string, user *protocol.MemoryUser, dialer internet.Dialer) (net.Conn, error) { + req := (&http.Request{ + Method: "CONNECT", + URL: &url.URL{Host: target}, + Header: make(http.Header), + Host: target, + }).WithContext(ctx) + if user != nil && user.Account != nil { account := user.Account.(*Account) auth := account.GetUsername() + ":" + account.GetPassword() - headers = append(headers, "Proxy-Authorization: Basic "+base64.StdEncoding.EncodeToString([]byte(auth))) + req.Header.Set("Proxy-Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(auth))) } - headers = append(headers, "Proxy-Connection: Keep-Alive") + req.Header.Set("Proxy-Connection", "Keep-Alive") - b := buf.New() - b.WriteString(strings.Join(headers, "\r\n") + "\r\n\r\n") - if err := buf.WriteAllBytes(writer, b.Bytes()); err != nil { - return err + connectHttp1 := func(rawConn net.Conn) (net.Conn, error) { + req.Proto = "HTTP/1.1" + req.ProtoMajor = 1 + req.ProtoMinor = 1 + + err := req.Write(rawConn) + if err != nil { + rawConn.Close() + return nil, err + } + + resp, err := http.ReadResponse(bufio.NewReader(rawConn), req) + if err != nil { + rawConn.Close() + return nil, err + } + + if resp.StatusCode != http.StatusOK { + rawConn.Close() + return nil, newError("Proxy responded with non 200 code: " + resp.Status) + } + return rawConn, nil } - b.Clear() - if _, err := b.ReadFrom(reader); err != nil { - return err + connectHttp2 := func(rawConn net.Conn, h2clientConn *http2.ClientConn) (net.Conn, error) { + req.Proto = "HTTP/2.0" + req.ProtoMajor = 2 + req.ProtoMinor = 0 + pr, pw := io.Pipe() + req.Body = pr + + resp, err := h2clientConn.RoundTrip(req) + if err != nil { + rawConn.Close() + return nil, err + } + + if resp.StatusCode != http.StatusOK { + rawConn.Close() + return nil, newError("Proxy responded with non 200 code: " + resp.Status) + } + return newHttp2Conn(rawConn, pw, resp.Body), nil } - return nil + cachedH2Mutex.Lock() + defer cachedH2Mutex.Unlock() + + if cachedConn, found := cachedH2Conns[dest]; found { + if cachedConn.rawConn != nil && cachedConn.h2Conn != nil { + rc := cachedConn.rawConn + cc := cachedConn.h2Conn + if cc.CanTakeNewRequest() { + proxyConn, err := connectHttp2(rc, cc) + if err != nil { + return nil, err + } + + return proxyConn, nil + } + } + } + + rawConn, err := dialer.Dial(ctx, dest) + if err != nil { + return nil, err + } + + nextProto := "" + if tlsConn, ok := rawConn.(*tls.Conn); ok { + if err := tlsConn.Handshake(); err != nil { + rawConn.Close() + return nil, err + } + nextProto = tlsConn.ConnectionState().NegotiatedProtocol + } + + switch nextProto { + case "": + fallthrough + case "http/1.1": + return connectHttp1(rawConn) + case "h2": + t := http2.Transport{} + h2clientConn, err := t.NewClientConn(rawConn) + if err != nil { + rawConn.Close() + return nil, err + } + + proxyConn, err := connectHttp2(rawConn, h2clientConn) + if err != nil { + rawConn.Close() + return nil, err + } + + if cachedH2Conns == nil { + cachedH2Conns = make(map[net.Destination]h2Conn) + } + + cachedH2Conns[dest] = h2Conn{ + rawConn: rawConn, + h2Conn: h2clientConn, + } + + return proxyConn, err + default: + return nil, newError("negotiated unsupported application layer protocol: " + nextProto) + } +} + +func newHttp2Conn(c net.Conn, pipedReqBody *io.PipeWriter, respBody io.ReadCloser) net.Conn { + return &http2Conn{Conn: c, in: pipedReqBody, out: respBody} +} + +type http2Conn struct { + net.Conn + in *io.PipeWriter + out io.ReadCloser +} + +func (h *http2Conn) Read(p []byte) (n int, err error) { + return h.out.Read(p) +} + +func (h *http2Conn) Write(p []byte) (n int, err error) { + return h.in.Write(p) +} + +func (h *http2Conn) Close() error { + h.in.Close() + return h.out.Close() +} + +func (h *http2Conn) CloseConn() error { + return h.Conn.Close() +} + +func (h *http2Conn) CloseWrite() error { + return h.in.Close() +} + +func (h *http2Conn) CloseRead() error { + return h.out.Close() } func init() { diff --git a/transport/internet/tls/tls.go b/transport/internet/tls/tls.go index 90b60e021..baf5d3933 100644 --- a/transport/internet/tls/tls.go +++ b/transport/internet/tls/tls.go @@ -14,25 +14,25 @@ import ( //go:generate errorgen var ( - _ buf.Writer = (*conn)(nil) + _ buf.Writer = (*Conn)(nil) ) -type conn struct { +type Conn struct { *tls.Conn } -func (c *conn) WriteMultiBuffer(mb buf.MultiBuffer) error { +func (c *Conn) WriteMultiBuffer(mb buf.MultiBuffer) error { mb = buf.Compact(mb) mb, err := buf.WriteMultiBuffer(c, mb) buf.ReleaseMulti(mb) return err } -func (c *conn) HandshakeAddress() net.Address { +func (c *Conn) HandshakeAddress() net.Address { if err := c.Handshake(); err != nil { return nil } - state := c.Conn.ConnectionState() + state := c.ConnectionState() if state.ServerName == "" { return nil } @@ -42,7 +42,7 @@ func (c *conn) HandshakeAddress() net.Address { // Client initiates a TLS client handshake on the given connection. func Client(c net.Conn, config *tls.Config) net.Conn { tlsConn := tls.Client(c, config) - return &conn{Conn: tlsConn} + return &Conn{Conn: tlsConn} } func copyConfig(c *tls.Config) *utls.Config { @@ -63,5 +63,5 @@ func UClient(c net.Conn, config *tls.Config) net.Conn { // Server initiates a TLS server handshake on the given connection. func Server(c net.Conn, config *tls.Config) net.Conn { tlsConn := tls.Server(c, config) - return &conn{Conn: tlsConn} + return &Conn{Conn: tlsConn} }