istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pkg/test/echo/server/forwarder/http.go (about)

     1  // Copyright Istio Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package forwarder
    16  
    17  import (
    18  	"bytes"
    19  	"context"
    20  	"crypto/tls"
    21  	"fmt"
    22  	"io"
    23  	"net"
    24  	"net/http"
    25  	"sort"
    26  	"strconv"
    27  	"strings"
    28  	"time"
    29  
    30  	"github.com/quic-go/quic-go"
    31  	"github.com/quic-go/quic-go/http3"
    32  	"golang.org/x/net/http2"
    33  
    34  	"istio.io/istio/pkg/hbone"
    35  	"istio.io/istio/pkg/test/echo"
    36  	"istio.io/istio/pkg/test/echo/common/scheme"
    37  	"istio.io/istio/pkg/test/echo/proto"
    38  )
    39  
    40  var _ protocol = &httpProtocol{}
    41  
    42  type httpProtocol struct {
    43  	e *executor
    44  }
    45  
    46  func newHTTPProtocol(e *executor) *httpProtocol {
    47  	return &httpProtocol{e: e}
    48  }
    49  
    50  type httpTransportGetter func() (http.RoundTripper, func(), error)
    51  
    52  func (c *httpProtocol) ForwardEcho(ctx context.Context, cfg *Config) (*proto.ForwardEchoResponse, error) {
    53  	var getTransport httpTransportGetter
    54  	var closeSharedTransport func()
    55  
    56  	switch {
    57  	case cfg.Request.Http3:
    58  		getTransport, closeSharedTransport = newHTTP3TransportGetter(cfg)
    59  	case cfg.Request.Http2:
    60  		getTransport, closeSharedTransport = newHTTP2TransportGetter(cfg)
    61  	default:
    62  		getTransport, closeSharedTransport = newHTTPTransportGetter(cfg)
    63  	}
    64  
    65  	defer closeSharedTransport()
    66  
    67  	call := &httpCall{
    68  		httpProtocol: c,
    69  		getTransport: getTransport,
    70  	}
    71  
    72  	return doForward(ctx, cfg, c.e, call.makeRequest)
    73  }
    74  
    75  func newHTTP3TransportGetter(cfg *Config) (httpTransportGetter, func()) {
    76  	newConn := func() *http3.RoundTripper {
    77  		return &http3.RoundTripper{
    78  			TLSClientConfig: cfg.tlsConfig,
    79  			QUICConfig:      &quic.Config{},
    80  		}
    81  	}
    82  	closeFn := func(conn *http3.RoundTripper) func() {
    83  		return func() {
    84  			_ = conn.Close()
    85  		}
    86  	}
    87  	noCloseFn := func() {}
    88  
    89  	if cfg.newConnectionPerRequest {
    90  		// Create a new transport (i.e. connection) for each request.
    91  		return func() (http.RoundTripper, func(), error) {
    92  			conn := newConn()
    93  			return conn, closeFn(conn), nil
    94  		}, noCloseFn
    95  	}
    96  
    97  	// Re-use the same transport for all requests. For HTTP3, this should result
    98  	// in multiplexing all requests over the same connection.
    99  	conn := newConn()
   100  	return func() (http.RoundTripper, func(), error) {
   101  		return conn, noCloseFn, nil
   102  	}, closeFn(conn)
   103  }
   104  
   105  func newHTTP2TransportGetter(cfg *Config) (httpTransportGetter, func()) {
   106  	newConn := func() *http2.Transport {
   107  		if cfg.scheme == scheme.HTTPS {
   108  			return &http2.Transport{
   109  				TLSClientConfig: cfg.tlsConfig,
   110  				DialTLS: func(network, addr string, tlsConfig *tls.Config) (net.Conn, error) {
   111  					return hbone.TLSDialWithDialer(newDialer(cfg), network, addr, tlsConfig)
   112  				},
   113  			}
   114  		}
   115  
   116  		return &http2.Transport{
   117  			// Golang doesn't have first class support for h2c, so we provide some workarounds
   118  			// See https://www.mailgun.com/blog/http-2-cleartext-h2c-client-example-go/
   119  			// So http2.Transport doesn't complain the URL scheme isn't 'https'
   120  			AllowHTTP: true,
   121  			// Pretend we are dialing a TLS endpoint. (Note, we ignore the passed tls.Config)
   122  			DialTLSContext: func(ctx context.Context, network, addr string, _ *tls.Config) (net.Conn, error) {
   123  				return newDialer(cfg).Dial(network, addr)
   124  			},
   125  		}
   126  	}
   127  	closeFn := func(conn *http2.Transport) func() {
   128  		return conn.CloseIdleConnections
   129  	}
   130  	noCloseFn := func() {}
   131  
   132  	if cfg.newConnectionPerRequest {
   133  		// Create a new transport (i.e. connection) for each request.
   134  		return func() (http.RoundTripper, func(), error) {
   135  			conn := newConn()
   136  			return conn, closeFn(conn), nil
   137  		}, noCloseFn
   138  	}
   139  
   140  	// Re-use the same transport for all requests. For HTTP2, this should result
   141  	// in multiplexing all requests over the same connection.
   142  	conn := newConn()
   143  	return func() (http.RoundTripper, func(), error) {
   144  		return conn, noCloseFn, nil
   145  	}, closeFn(conn)
   146  }
   147  
   148  func newHTTPTransportGetter(cfg *Config) (httpTransportGetter, func()) {
   149  	newConn := func() *http.Transport {
   150  		dialContext := func(ctx context.Context, network, addr string) (net.Conn, error) {
   151  			return newDialer(cfg).DialContext(ctx, network, addr)
   152  		}
   153  		if len(cfg.UDS) > 0 {
   154  			dialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
   155  				return newDialer(cfg).DialContext(ctx, "unix", cfg.UDS)
   156  			}
   157  		}
   158  		out := &http.Transport{
   159  			// No connection pooling.
   160  			DisableKeepAlives: true,
   161  			TLSClientConfig:   cfg.tlsConfig,
   162  			DialContext:       dialContext,
   163  		}
   164  
   165  		// Set the proxy in the transport, if specified.
   166  		// for socks5 proxy is setup is done in the newDialer function.
   167  		if !strings.HasPrefix(cfg.Proxy, "socks5://") {
   168  			out.Proxy = cfg.proxyURL
   169  		}
   170  		return out
   171  	}
   172  	noCloseFn := func() {}
   173  
   174  	// Always create a new HTTP transport for each request, since HTTP can't multiplex over
   175  	// a single connection.
   176  	return func() (http.RoundTripper, func(), error) {
   177  		conn := newConn()
   178  		return conn, noCloseFn, nil
   179  	}, noCloseFn
   180  }
   181  
   182  type httpCall struct {
   183  	*httpProtocol
   184  	getTransport httpTransportGetter
   185  }
   186  
   187  func (c *httpCall) makeRequest(ctx context.Context, cfg *Config, requestID int) (string, error) {
   188  	start := time.Now()
   189  
   190  	r := cfg.Request
   191  	var outBuffer bytes.Buffer
   192  	echo.ForwarderURLField.WriteForRequest(&outBuffer, requestID, r.Url)
   193  
   194  	// Set the per-request timeout.
   195  	ctx, cancel := context.WithTimeout(ctx, cfg.timeout)
   196  	defer cancel()
   197  
   198  	httpReq, err := http.NewRequestWithContext(ctx, cfg.method, cfg.urlHost, nil)
   199  	if err != nil {
   200  		return outBuffer.String(), err
   201  	}
   202  
   203  	// Use raw path, we don't want golang normalizing anything since we use this for testing purposes
   204  	httpReq.URL.Opaque = cfg.urlPath
   205  
   206  	// Use the host header as the host.
   207  	httpReq.Host = cfg.hostHeader
   208  
   209  	// Copy the headers.
   210  	httpReq.Header = cfg.headers.Clone()
   211  	writeForwardedHeaders(&outBuffer, requestID, cfg.headers)
   212  
   213  	// Propagate previous response cookies if any
   214  	if cfg.PropagateResponse != nil {
   215  		cfg.PropagateResponse(httpReq, cfg.previousResponse)
   216  	}
   217  	// Get the transport.
   218  	transport, closeTransport, err := c.getTransport()
   219  	if err != nil {
   220  		return outBuffer.String(), err
   221  	}
   222  	defer closeTransport()
   223  
   224  	// Create a new HTTP client.
   225  	client := &http.Client{
   226  		CheckRedirect: cfg.checkRedirect,
   227  		Timeout:       cfg.timeout,
   228  		Transport:     transport,
   229  	}
   230  
   231  	// Make the request.
   232  	httpResp, err := client.Do(httpReq)
   233  	if err != nil {
   234  		return outBuffer.String(), err
   235  	}
   236  	cfg.previousResponse = httpResp
   237  
   238  	echo.LatencyField.WriteForRequest(&outBuffer, requestID, fmt.Sprintf("%v", time.Since(start)))
   239  	echo.ActiveRequestsField.WriteForRequest(&outBuffer, requestID, fmt.Sprintf("%d", c.e.ActiveRequests()))
   240  
   241  	// Process the response.
   242  	err = processHTTPResponse(requestID, httpResp, &outBuffer)
   243  
   244  	// Extract the output string.
   245  	return outBuffer.String(), err
   246  }
   247  
   248  func processHTTPResponse(requestID int, httpResp *http.Response, outBuffer *bytes.Buffer) error {
   249  	// Make sure we close the body before exiting.
   250  	defer func() {
   251  		if err := httpResp.Body.Close(); err != nil {
   252  			echo.WriteError(outBuffer, requestID, err)
   253  		}
   254  	}()
   255  
   256  	echo.StatusCodeField.WriteForRequest(outBuffer, requestID, strconv.Itoa(httpResp.StatusCode))
   257  
   258  	// Read the entire body.
   259  	data, err := io.ReadAll(httpResp.Body)
   260  	if err != nil {
   261  		return err
   262  	}
   263  
   264  	// Write the response headers to the output buffer.
   265  	var keys []string
   266  	for k := range httpResp.Header {
   267  		keys = append(keys, k)
   268  	}
   269  	sort.Strings(keys)
   270  	for _, key := range keys {
   271  		values := httpResp.Header[key]
   272  		for _, value := range values {
   273  			echo.ResponseHeaderField.WriteKeyValueForRequest(outBuffer, requestID, key, value)
   274  		}
   275  	}
   276  
   277  	// Write the lines of the body to the output buffer.
   278  	for _, line := range strings.Split(string(data), "\n") {
   279  		if line != "" {
   280  			echo.WriteBodyLine(outBuffer, requestID, line)
   281  		}
   282  	}
   283  	return nil
   284  }
   285  
   286  func (c *httpProtocol) Close() error {
   287  	return nil
   288  }