go.uber.org/yarpc@v1.72.1/transport/http/inbound.go (about)

     1  // Copyright (c) 2022 Uber Technologies, Inc.
     2  //
     3  // Permission is hereby granted, free of charge, to any person obtaining a copy
     4  // of this software and associated documentation files (the "Software"), to deal
     5  // in the Software without restriction, including without limitation the rights
     6  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
     7  // copies of the Software, and to permit persons to whom the Software is
     8  // furnished to do so, subject to the following conditions:
     9  //
    10  // The above copyright notice and this permission notice shall be included in
    11  // all copies or substantial portions of the Software.
    12  //
    13  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    14  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    15  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    16  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    17  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    18  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    19  // THE SOFTWARE.
    20  
    21  package http
    22  
    23  import (
    24  	"context"
    25  	"crypto/tls"
    26  	"errors"
    27  	"net"
    28  	"net/http"
    29  	"strings"
    30  	"time"
    31  
    32  	"github.com/opentracing/opentracing-go"
    33  	"go.uber.org/yarpc/api/transport"
    34  	yarpctls "go.uber.org/yarpc/api/transport/tls"
    35  	"go.uber.org/yarpc/api/x/introspection"
    36  	intnet "go.uber.org/yarpc/internal/net"
    37  	"go.uber.org/yarpc/pkg/lifecycle"
    38  	"go.uber.org/yarpc/transport/internal/tls/muxlistener"
    39  	"go.uber.org/yarpc/yarpcerrors"
    40  	"go.uber.org/zap"
    41  )
    42  
    43  // We want a value that's around 5 seconds, but slightly higher than how
    44  // long a successful HTTP shutdown can take.
    45  // There's a specific path in the HTTP shutdown path that can take 5 seconds:
    46  // https://golang.org/src/net/http/server.go?s=83923:83977#L2710
    47  // This avoids timeouts in shutdown caused by new idle connections, without
    48  // making the timeout too large.
    49  const defaultShutdownTimeout = 6 * time.Second
    50  
    51  // InboundOption customizes the behavior of an HTTP Inbound constructed with
    52  // NewInbound.
    53  type InboundOption func(*Inbound)
    54  
    55  func (InboundOption) httpOption() {}
    56  
    57  // Mux specifies that the HTTP server should make the YARPC endpoint available
    58  // under the given pattern on the given ServeMux. By default, the YARPC
    59  // service is made available on all paths of the HTTP server. By specifying a
    60  // ServeMux, users can narrow the endpoints under which the YARPC service is
    61  // available and offer their own non-YARPC endpoints.
    62  func Mux(pattern string, mux *http.ServeMux) InboundOption {
    63  	return func(i *Inbound) {
    64  		i.mux = mux
    65  		i.muxPattern = pattern
    66  	}
    67  }
    68  
    69  // Interceptor specifies a function which can wrap the YARPC handler. If
    70  // provided, this function will be called with an http.Handler which will
    71  // route requests through YARPC. The http.Handler returned by this function
    72  // may delegate requests to the provided YARPC handler to route them through
    73  // YARPC.
    74  // Interceptors are applied in LIFO order, leading to an earlier interceptor's
    75  // handler being executed before latter interceptor handlers
    76  func Interceptor(interceptor func(yarpcHandler http.Handler) http.Handler) InboundOption {
    77  	return func(i *Inbound) {
    78  		i.interceptors = append(i.interceptors, interceptor)
    79  	}
    80  }
    81  
    82  // GrabHeaders specifies additional headers that are not prefixed with
    83  // ApplicationHeaderPrefix that should be propagated to the caller.
    84  //
    85  // All headers given must begin with x- or X- or the Inbound that the
    86  // returned option is passed to will return an error when Start is called.
    87  //
    88  // Headers specified with GrabHeaders are case-insensitive.
    89  // https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2
    90  func GrabHeaders(headers ...string) InboundOption {
    91  	return func(i *Inbound) {
    92  		for _, header := range headers {
    93  			i.grabHeaders[strings.ToLower(header)] = struct{}{}
    94  		}
    95  	}
    96  }
    97  
    98  // ShutdownTimeout specifies the maximum duration the inbound should wait for
    99  // closing idle connections, and pending calls to complete.
   100  //
   101  // Set to 0 to wait for a complete drain.
   102  //
   103  // Defaults to 5 seconds.
   104  func ShutdownTimeout(timeout time.Duration) InboundOption {
   105  	return func(i *Inbound) {
   106  		i.shutdownTimeout = timeout
   107  	}
   108  }
   109  
   110  // InboundTLSConfiguration returns an InboundOption that provides the TLS
   111  // confiugration used for setting up TLS inbound.
   112  func InboundTLSConfiguration(tlsConfig *tls.Config) InboundOption {
   113  	return func(i *Inbound) {
   114  		i.tlsConfig = tlsConfig
   115  	}
   116  }
   117  
   118  // InboundTLSMode returns an InboundOption that sets inbound TLS mode.
   119  // It must be noted that TLS configuration must be passed separately using inbound
   120  // option InboundTLSConfiguration.
   121  func InboundTLSMode(mode yarpctls.Mode) InboundOption {
   122  	return func(i *Inbound) {
   123  		i.tlsMode = mode
   124  	}
   125  }
   126  
   127  // NewInbound builds a new HTTP inbound that listens on the given address and
   128  // sharing this transport.
   129  func (t *Transport) NewInbound(addr string, opts ...InboundOption) *Inbound {
   130  	i := &Inbound{
   131  		once:              lifecycle.NewOnce(),
   132  		addr:              addr,
   133  		shutdownTimeout:   defaultShutdownTimeout,
   134  		tracer:            t.tracer,
   135  		logger:            t.logger,
   136  		transport:         t,
   137  		grabHeaders:       make(map[string]struct{}),
   138  		bothResponseError: true,
   139  	}
   140  	for _, opt := range opts {
   141  		opt(i)
   142  	}
   143  	return i
   144  }
   145  
   146  // Inbound receives YARPC requests using an HTTP server. It may be constructed
   147  // using the NewInbound method on the Transport.
   148  type Inbound struct {
   149  	addr            string
   150  	mux             *http.ServeMux
   151  	muxPattern      string
   152  	server          *intnet.HTTPServer
   153  	shutdownTimeout time.Duration
   154  	router          transport.Router
   155  	tracer          opentracing.Tracer
   156  	logger          *zap.Logger
   157  	transport       *Transport
   158  	grabHeaders     map[string]struct{}
   159  	interceptors    []func(http.Handler) http.Handler
   160  
   161  	once *lifecycle.Once
   162  
   163  	// should only be false in testing
   164  	bothResponseError bool
   165  
   166  	tlsConfig *tls.Config
   167  	tlsMode   yarpctls.Mode
   168  }
   169  
   170  // Tracer configures a tracer on this inbound.
   171  func (i *Inbound) Tracer(tracer opentracing.Tracer) *Inbound {
   172  	i.tracer = tracer
   173  	return i
   174  }
   175  
   176  // SetRouter configures a router to handle incoming requests.
   177  // This satisfies the transport.Inbound interface, and would be called
   178  // by a dispatcher when it starts.
   179  func (i *Inbound) SetRouter(router transport.Router) {
   180  	i.router = router
   181  }
   182  
   183  // Transports returns the inbound's HTTP transport.
   184  func (i *Inbound) Transports() []transport.Transport {
   185  	return []transport.Transport{i.transport}
   186  }
   187  
   188  // Start starts the inbound with a given service detail, opening a listening
   189  // socket.
   190  func (i *Inbound) Start() error {
   191  	return i.once.Start(i.start)
   192  }
   193  
   194  func (i *Inbound) start() error {
   195  	if i.router == nil {
   196  		return yarpcerrors.Newf(yarpcerrors.CodeInternal, "no router configured for transport inbound")
   197  	}
   198  	for header := range i.grabHeaders {
   199  		if !strings.HasPrefix(header, "x-") {
   200  			return yarpcerrors.Newf(yarpcerrors.CodeInvalidArgument, "header %s does not begin with 'x-'", header)
   201  		}
   202  	}
   203  
   204  	var httpHandler http.Handler = handler{
   205  		router:            i.router,
   206  		tracer:            i.tracer,
   207  		grabHeaders:       i.grabHeaders,
   208  		bothResponseError: i.bothResponseError,
   209  		logger:            i.logger,
   210  	}
   211  
   212  	// reverse iterating because we want the last from options to wrap the
   213  	// the underlying yarpc http handlers.
   214  	// This way, the first from the option will be the
   215  	// outermost wrapper http.Handler and it will be invoked first during request handling.
   216  	for j := len(i.interceptors) - 1; j >= 0; j-- {
   217  		httpHandler = i.interceptors[j](httpHandler)
   218  	}
   219  	if i.mux != nil {
   220  		i.mux.Handle(i.muxPattern, httpHandler)
   221  		httpHandler = i.mux
   222  	}
   223  
   224  	i.server = intnet.NewHTTPServer(&http.Server{
   225  		Addr:    i.addr,
   226  		Handler: httpHandler,
   227  	})
   228  
   229  	addr := i.addr
   230  	if addr == "" {
   231  		addr = ":http"
   232  	}
   233  
   234  	listener, err := net.Listen("tcp", addr)
   235  	if err != nil {
   236  		return err
   237  	}
   238  
   239  	if i.tlsMode != yarpctls.Disabled {
   240  		if i.tlsConfig == nil {
   241  			return errors.New("HTTP TLS enabled but configuration not provided")
   242  		}
   243  
   244  		listener = muxlistener.NewListener(muxlistener.Config{
   245  			Listener:      listener,
   246  			TLSConfig:     i.tlsConfig,
   247  			ServiceName:   i.transport.serviceName,
   248  			TransportName: TransportName,
   249  			Meter:         i.transport.meter,
   250  			Logger:        i.logger,
   251  			Mode:          i.tlsMode,
   252  		})
   253  	}
   254  
   255  	if err := i.server.Serve(listener); err != nil {
   256  		return err
   257  	}
   258  
   259  	i.addr = i.server.Listener().Addr().String() // in case it changed
   260  	i.logger.Info("started HTTP inbound", zap.String("address", i.addr))
   261  	if len(i.router.Procedures()) == 0 {
   262  		i.logger.Warn("no procedures specified for HTTP inbound")
   263  	}
   264  	return nil
   265  }
   266  
   267  // Stop the inbound using Shutdown.
   268  func (i *Inbound) Stop() error {
   269  	ctx, cancel := context.WithTimeout(context.Background(), i.shutdownTimeout)
   270  	defer cancel()
   271  
   272  	return i.shutdown(ctx)
   273  }
   274  
   275  // shutdown the inbound, closing the listening socket, closing idle
   276  // connections, and waiting for all pending calls to complete.
   277  func (i *Inbound) shutdown(ctx context.Context) error {
   278  	return i.once.Stop(func() error {
   279  		if i.server == nil {
   280  			return nil
   281  		}
   282  
   283  		return i.server.Shutdown(ctx)
   284  	})
   285  }
   286  
   287  // IsRunning returns whether the inbound is currently running
   288  func (i *Inbound) IsRunning() bool {
   289  	return i.once.IsRunning()
   290  }
   291  
   292  // Addr returns the address on which the server is listening. Returns nil if
   293  // Start has not been called yet.
   294  func (i *Inbound) Addr() net.Addr {
   295  	if i.server == nil {
   296  		return nil
   297  	}
   298  
   299  	listener := i.server.Listener()
   300  	if listener == nil {
   301  		return nil
   302  	}
   303  
   304  	return listener.Addr()
   305  }
   306  
   307  // Introspect returns the state of the inbound for introspection purposes.
   308  func (i *Inbound) Introspect() introspection.InboundStatus {
   309  	state := "Stopped"
   310  	if i.IsRunning() {
   311  		state = "Started"
   312  	}
   313  	var addrString string
   314  	if addr := i.Addr(); addr != nil {
   315  		addrString = addr.String()
   316  	}
   317  	return introspection.InboundStatus{
   318  		Transport: "http",
   319  		Endpoint:  addrString,
   320  		State:     state,
   321  	}
   322  }