go.uber.org/yarpc@v1.72.1/transport/http/config.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  	"errors"
    25  	"fmt"
    26  	"time"
    27  
    28  	"go.uber.org/yarpc/api/transport"
    29  	yarpctls "go.uber.org/yarpc/api/transport/tls"
    30  	"go.uber.org/yarpc/peer/hostport"
    31  	"go.uber.org/yarpc/yarpcconfig"
    32  )
    33  
    34  // TransportSpec returns a TransportSpec for the HTTP transport.
    35  //
    36  // See TransportConfig, InboundConfig, and OutboundConfig for details on the
    37  // different configuration parameters supported by this Transport.
    38  //
    39  // Any Transport, Inbound or Outbound option may be passed to this function.
    40  // These options will be applied BEFORE configuration parameters are
    41  // interpreted. This allows configuration parameters to override Option
    42  // provided to TransportSpec.
    43  func TransportSpec(opts ...Option) yarpcconfig.TransportSpec {
    44  	var ts transportSpec
    45  	for _, o := range opts {
    46  		switch opt := o.(type) {
    47  		case TransportOption:
    48  			ts.TransportOptions = append(ts.TransportOptions, opt)
    49  		case InboundOption:
    50  			ts.InboundOptions = append(ts.InboundOptions, opt)
    51  		case OutboundOption:
    52  			ts.OutboundOptions = append(ts.OutboundOptions, opt)
    53  		default:
    54  			panic(fmt.Sprintf("unknown option of type %T: %v", o, o))
    55  		}
    56  	}
    57  	return ts.Spec()
    58  }
    59  
    60  // transportSpec holds the configurable parts of the HTTP TransportSpec.
    61  //
    62  // These are usually runtime dependencies that cannot be parsed from
    63  // configuration.
    64  type transportSpec struct {
    65  	TransportOptions []TransportOption
    66  	InboundOptions   []InboundOption
    67  	OutboundOptions  []OutboundOption
    68  }
    69  
    70  func (ts *transportSpec) Spec() yarpcconfig.TransportSpec {
    71  	return yarpcconfig.TransportSpec{
    72  		Name:                TransportName,
    73  		BuildTransport:      ts.buildTransport,
    74  		BuildInbound:        ts.buildInbound,
    75  		BuildUnaryOutbound:  ts.buildUnaryOutbound,
    76  		BuildOnewayOutbound: ts.buildOnewayOutbound,
    77  	}
    78  }
    79  
    80  // TransportConfig configures the shared HTTP Transport. This is shared
    81  // between all HTTP outbounds and inbounds of a Dispatcher.
    82  //
    83  //  transports:
    84  //    http:
    85  //      keepAlive: 30s
    86  //      maxIdleConns: 2
    87  //      maxIdleConnsPerHost: 2
    88  //      idleConnTimeout: 90s
    89  //      disableKeepAlives: false
    90  //      disableCompression: false
    91  //      responseHeaderTimeout: 0s
    92  //      connTimeout: 500ms
    93  //      connBackoff:
    94  //        exponential:
    95  //          first: 10ms
    96  //          max: 30s
    97  //
    98  // All parameters of TransportConfig are optional. This section may be omitted
    99  // in the transports section.
   100  type TransportConfig struct {
   101  	// Specifies the keep-alive period for all HTTP clients. This field is
   102  	// optional.
   103  	KeepAlive             time.Duration       `config:"keepAlive"`
   104  	MaxIdleConns          int                 `config:"maxIdleConns"`
   105  	MaxIdleConnsPerHost   int                 `config:"maxIdleConnsPerHost"`
   106  	IdleConnTimeout       time.Duration       `config:"idleConnTimeout"`
   107  	DisableKeepAlives     bool                `config:"disableKeepAlives"`
   108  	DisableCompression    bool                `config:"disableCompression"`
   109  	ResponseHeaderTimeout time.Duration       `config:"responseHeaderTimeout"`
   110  	ConnTimeout           time.Duration       `config:"connTimeout"`
   111  	ConnBackoff           yarpcconfig.Backoff `config:"connBackoff"`
   112  }
   113  
   114  func (ts *transportSpec) buildTransport(tc *TransportConfig, k *yarpcconfig.Kit) (transport.Transport, error) {
   115  	options := newTransportOptions()
   116  
   117  	for _, opt := range ts.TransportOptions {
   118  		opt(&options)
   119  	}
   120  
   121  	if options.serviceName == "" {
   122  		options.serviceName = k.ServiceName()
   123  	}
   124  	if tc.KeepAlive > 0 {
   125  		options.keepAlive = tc.KeepAlive
   126  	}
   127  	if tc.MaxIdleConns > 0 {
   128  		options.maxIdleConns = tc.MaxIdleConns
   129  	}
   130  	if tc.MaxIdleConnsPerHost > 0 {
   131  		options.maxIdleConnsPerHost = tc.MaxIdleConnsPerHost
   132  	}
   133  	if tc.IdleConnTimeout > 0 {
   134  		options.idleConnTimeout = tc.IdleConnTimeout
   135  	}
   136  	if tc.DisableKeepAlives {
   137  		options.disableKeepAlives = true
   138  	}
   139  	if tc.DisableCompression {
   140  		options.disableCompression = true
   141  	}
   142  	if tc.ResponseHeaderTimeout > 0 {
   143  		options.responseHeaderTimeout = tc.ResponseHeaderTimeout
   144  	}
   145  	if tc.ConnTimeout > 0 {
   146  		options.connTimeout = tc.ConnTimeout
   147  	}
   148  
   149  	strategy, err := tc.ConnBackoff.Strategy()
   150  	if err != nil {
   151  		return nil, err
   152  	}
   153  	options.connBackoffStrategy = strategy
   154  
   155  	return options.newTransport(), nil
   156  }
   157  
   158  // InboundConfig configures an HTTP inbound.
   159  //
   160  //  inbounds:
   161  //    http:
   162  //      address: ":80"
   163  //      grabHeaders:
   164  //        - x-foo
   165  //        - x-bar
   166  //      shutdownTimeout: 5s
   167  type InboundConfig struct {
   168  	// Address to listen on. This field is required.
   169  	Address string `config:"address,interpolate"`
   170  	// The additional headers, starting with x, that should be
   171  	// propagated to handlers. This field is optional.
   172  	GrabHeaders []string `config:"grabHeaders"`
   173  	// The maximum amount of time to wait for the inbound to shutdown.
   174  	ShutdownTimeout *time.Duration `config:"shutdownTimeout"`
   175  	// TLS configuration of the inbound.
   176  	TLSConfig TLSConfig `config:"tls"`
   177  }
   178  
   179  // TLSConfig specifies the TLS configuration of the HTTP inbound.
   180  type TLSConfig struct {
   181  	// Mode when set to Permissive or Enforced enables TLS inbound and
   182  	// TLS configuration must be passed as an inbound option.
   183  	Mode yarpctls.Mode `config:"mode,interpolate"`
   184  }
   185  
   186  func (ts *transportSpec) buildInbound(ic *InboundConfig, t transport.Transport, k *yarpcconfig.Kit) (transport.Inbound, error) {
   187  	if ic.Address == "" {
   188  		return nil, fmt.Errorf("inbound address is required")
   189  	}
   190  
   191  	// TLS mode provided in the inbound options takes higher precedence than
   192  	// the TLS mode passed in YAML config.
   193  	inboundOptions := append([]InboundOption{InboundTLSMode(ic.TLSConfig.Mode)}, ts.InboundOptions...)
   194  	if len(ic.GrabHeaders) > 0 {
   195  		inboundOptions = append(inboundOptions, GrabHeaders(ic.GrabHeaders...))
   196  	}
   197  
   198  	if ic.ShutdownTimeout != nil {
   199  		if *ic.ShutdownTimeout < 0 {
   200  			return nil, fmt.Errorf("shutdownTimeout must not be negative, got: %q", ic.ShutdownTimeout)
   201  		}
   202  		inboundOptions = append(inboundOptions, ShutdownTimeout(*ic.ShutdownTimeout))
   203  	}
   204  
   205  	return t.(*Transport).NewInbound(ic.Address, inboundOptions...), nil
   206  }
   207  
   208  // OutboundConfig configures an HTTP outbound.
   209  //
   210  //  outbounds:
   211  //    keyvalueservice:
   212  //      http:
   213  //        url: "http://127.0.0.1:80/"
   214  //
   215  // The HTTP outbound supports both, Unary and Oneway transport types. To use
   216  // it for only one of these, nest the section inside a "unary" or "onewy"
   217  // section.
   218  //
   219  //  outbounds:
   220  //    keyvalueservice:
   221  //      unary:
   222  //        http:
   223  //          url: "http://127.0.0.1:80/"
   224  //
   225  // An HTTP outbound can also configure a peer list.
   226  // In this case, there can still be a "url" and it serves as a template for the
   227  // HTTP client, expressing whether to use "http:" or "https:" and what path to
   228  // use. The address gets replaced with peers from the peer list.
   229  //
   230  //  outbounds:
   231  //    keyvalueservice:
   232  //      unary:
   233  //        http:
   234  //          url: "https://address/rpc"
   235  //          round-robin:
   236  //            peers:
   237  //              - 127.0.0.1:8080
   238  //              - 127.0.0.1:8081
   239  type OutboundConfig struct {
   240  	yarpcconfig.PeerChooser
   241  
   242  	// URL to which requests will be sent for this outbound. This field is
   243  	// required.
   244  	URL string `config:"url,interpolate"`
   245  	// HTTP headers that will be added to all requests made through this
   246  	// outbound.
   247  	//
   248  	//  http:
   249  	//    url: "http://localhost:8080/yarpc"
   250  	//    addHeaders:
   251  	//      X-Caller: myserice
   252  	//      X-Token: foo
   253  	AddHeaders map[string]string `config:"addHeaders"`
   254  	// TLS config enables TLS outbound.
   255  	//
   256  	//  http:
   257  	//    url: "http://localhost:8080/yarpc"
   258  	//    tls:
   259  	//      mode: enforced
   260  	//      spiffe-ids:
   261  	//        - destination-id
   262  	TLS OutboundTLSConfig `config:"tls"`
   263  }
   264  
   265  // OutboundTLSConfig configures TLS for the HTTP outbound.
   266  type OutboundTLSConfig struct {
   267  	// Mode when set to Enforced enables outbound TLS.
   268  	// Note: outbound TLS configuration provider must be given as an option
   269  	// which is used for fetching client tls.Config.
   270  	Mode yarpctls.Mode `config:"mode,interpolate"`
   271  	// SpiffeIDs is list of accepted server spiffe IDs. This cannot be empty
   272  	// list.
   273  	SpiffeIDs []string `config:"spiffe-ids"`
   274  }
   275  
   276  func (o OutboundTLSConfig) options(provider yarpctls.OutboundTLSConfigProvider) ([]OutboundOption, error) {
   277  	if o.Mode == yarpctls.Disabled {
   278  		return nil, nil
   279  	}
   280  
   281  	if o.Mode == yarpctls.Permissive {
   282  		return nil, errors.New("outbound does not support permissive TLS mode")
   283  	}
   284  
   285  	if provider == nil {
   286  		return nil, errors.New("outbound TLS enforced but outbound TLS config provider is nil")
   287  	}
   288  
   289  	config, err := provider.ClientTLSConfig(o.SpiffeIDs)
   290  	if err != nil {
   291  		return nil, err
   292  	}
   293  
   294  	return []OutboundOption{OutboundTLSConfiguration(config)}, nil
   295  }
   296  
   297  func (ts *transportSpec) buildOutbound(oc *OutboundConfig, t transport.Transport, k *yarpcconfig.Kit) (*Outbound, error) {
   298  	x := t.(*Transport)
   299  
   300  	opts := []OutboundOption{OutboundDestinationServiceName(k.OutboundServiceName())}
   301  	opts = append(opts, ts.OutboundOptions...)
   302  	if len(oc.AddHeaders) > 0 {
   303  		for k, v := range oc.AddHeaders {
   304  			opts = append(opts, AddHeader(k, v))
   305  		}
   306  	}
   307  
   308  	option, err := oc.TLS.options(x.ouboundTLSConfigProvider)
   309  	if err != nil {
   310  		return nil, err
   311  	}
   312  	opts = append(option, opts...)
   313  
   314  	// Special case where the URL implies the single peer.
   315  	if oc.Empty() {
   316  		return x.NewSingleOutbound(oc.URL, opts...), nil
   317  	}
   318  
   319  	chooser, err := oc.BuildPeerChooser(x, hostport.Identify, k)
   320  	if err != nil {
   321  		return nil, fmt.Errorf("cannot configure peer chooser for HTTP outbound: %v", err)
   322  	}
   323  
   324  	if oc.URL != "" {
   325  		opts = append(opts, URLTemplate(oc.URL))
   326  	}
   327  	return x.NewOutbound(chooser, opts...), nil
   328  }
   329  
   330  func (ts *transportSpec) buildUnaryOutbound(oc *OutboundConfig, t transport.Transport, k *yarpcconfig.Kit) (transport.UnaryOutbound, error) {
   331  	return ts.buildOutbound(oc, t, k)
   332  }
   333  
   334  func (ts *transportSpec) buildOnewayOutbound(oc *OutboundConfig, t transport.Transport, k *yarpcconfig.Kit) (transport.OnewayOutbound, error) {
   335  	return ts.buildOutbound(oc, t, k)
   336  }