go.uber.org/yarpc@v1.72.1/transport/grpc/outbound.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 grpc
    22  
    23  import (
    24  	"bytes"
    25  	"context"
    26  	"io/ioutil"
    27  	"strings"
    28  	"time"
    29  
    30  	"github.com/gogo/status"
    31  	"github.com/opentracing/opentracing-go"
    32  	"go.uber.org/yarpc"
    33  	"go.uber.org/yarpc/api/peer"
    34  	"go.uber.org/yarpc/api/transport"
    35  	"go.uber.org/yarpc/api/x/introspection"
    36  	"go.uber.org/yarpc/internal/grpcerrorcodes"
    37  	intyarpcerrors "go.uber.org/yarpc/internal/yarpcerrors"
    38  	peerchooser "go.uber.org/yarpc/peer"
    39  	"go.uber.org/yarpc/peer/hostport"
    40  	"go.uber.org/yarpc/pkg/lifecycle"
    41  	"go.uber.org/yarpc/yarpcerrors"
    42  	"google.golang.org/grpc"
    43  	"google.golang.org/grpc/metadata"
    44  )
    45  
    46  // UserAgent is the User-Agent that will be set for requests.
    47  // http://www.grpc.io/docs/guides/wire.html#user-agents
    48  const UserAgent = "yarpc-go/" + yarpc.Version
    49  
    50  var (
    51  	_                         transport.UnaryOutbound              = (*Outbound)(nil)
    52  	_                         introspection.IntrospectableOutbound = (*Outbound)(nil)
    53  	invalidHeaderValueCharSet                                      = "\r\n" + string('\x00') // NUL
    54  )
    55  
    56  // Outbound is a transport.UnaryOutbound.
    57  type Outbound struct {
    58  	once        *lifecycle.Once
    59  	t           *Transport
    60  	peerChooser peer.Chooser
    61  	options     *outboundOptions
    62  }
    63  
    64  func newSingleOutbound(t *Transport, address string, options ...OutboundOption) *Outbound {
    65  	return newOutbound(t, peerchooser.NewSingle(hostport.PeerIdentifier(address), t), options...)
    66  }
    67  
    68  func newOutbound(t *Transport, peerChooser peer.Chooser, options ...OutboundOption) *Outbound {
    69  	return &Outbound{
    70  		once:        lifecycle.NewOnce(),
    71  		t:           t,
    72  		peerChooser: peerChooser,
    73  		options:     newOutboundOptions(options),
    74  	}
    75  }
    76  
    77  // TransportName is the transport name that will be set on `transport.Request`
    78  // struct.
    79  func (o *Outbound) TransportName() string {
    80  	return TransportName
    81  }
    82  
    83  // Start implements transport.Lifecycle#Start.
    84  func (o *Outbound) Start() error {
    85  	return o.once.Start(o.peerChooser.Start)
    86  }
    87  
    88  // Stop implements transport.Lifecycle#Stop.
    89  func (o *Outbound) Stop() error {
    90  	return o.once.Stop(o.peerChooser.Stop)
    91  }
    92  
    93  // IsRunning implements transport.Lifecycle#IsRunning.
    94  func (o *Outbound) IsRunning() bool {
    95  	return o.once.IsRunning()
    96  }
    97  
    98  // Transports implements transport.Inbound#Transports.
    99  func (o *Outbound) Transports() []transport.Transport {
   100  	return []transport.Transport{o.t}
   101  }
   102  
   103  // Chooser returns the peer.Chooser associated with this Outbound.
   104  func (o *Outbound) Chooser() peer.Chooser {
   105  	return o.peerChooser
   106  }
   107  
   108  // Call implements transport.UnaryOutbound#Call.
   109  func (o *Outbound) Call(ctx context.Context, request *transport.Request) (*transport.Response, error) {
   110  	if request == nil {
   111  		return nil, yarpcerrors.InvalidArgumentErrorf("request for grpc outbound was nil")
   112  	}
   113  	if err := validateRequest(request); err != nil {
   114  		return nil, err
   115  	}
   116  	if err := o.once.WaitUntilRunning(ctx); err != nil {
   117  		return nil, intyarpcerrors.AnnotateWithInfo(yarpcerrors.FromError(err), "error waiting for grpc outbound to start for service: %s", request.Service)
   118  	}
   119  	start := time.Now()
   120  
   121  	var responseBody []byte
   122  	var responseMD metadata.MD
   123  	invokeErr := o.invoke(ctx, request, &responseBody, &responseMD, start)
   124  
   125  	responseHeaders, err := getApplicationHeaders(responseMD)
   126  	if err != nil {
   127  		return nil, err
   128  	}
   129  	return &transport.Response{
   130  		Body:                 ioutil.NopCloser(bytes.NewReader(responseBody)),
   131  		BodySize:             len(responseBody),
   132  		Headers:              responseHeaders,
   133  		ApplicationError:     metadataToIsApplicationError(responseMD),
   134  		ApplicationErrorMeta: metadataToApplicationErrorMeta(responseMD),
   135  	}, invokeErr
   136  }
   137  
   138  func validateRequest(req *transport.Request) error {
   139  	for _, v := range req.Headers.Items() {
   140  		// from https://httpwg.org/specs/rfc7540.html#rfc.section.10.3:
   141  		// HTTP/2 allows header field values that are not valid.
   142  		// While most of the values that can be encoded will not alter header field parsing,
   143  		// carriage return (CR, ASCII 0xd), line feed (LF, ASCII 0xa),
   144  		// and the zero character (NUL, ASCII 0x0) might be exploited
   145  		// by an attacker if they are translated verbatim.
   146  		// This should be done by grpc-go but the workaround in https://github.com/grpc/grpc-go/pull/610
   147  		// does not cover this case.
   148  		// This validation can be entirely removed if the https://github.com/grpc/grpc/issues/4672
   149  		// is solved properly.
   150  		if strings.ContainsAny(v, invalidHeaderValueCharSet) {
   151  			return yarpcerrors.InvalidArgumentErrorf("grpc request header value contains invalid characters including ASCII 0xd, 0xa, or 0x0")
   152  		}
   153  	}
   154  	return nil
   155  }
   156  
   157  func (o *Outbound) invoke(
   158  	ctx context.Context,
   159  	request *transport.Request,
   160  	responseBody *[]byte,
   161  	responseMD *metadata.MD,
   162  	start time.Time,
   163  ) (retErr error) {
   164  	md, err := transportRequestToMetadata(request)
   165  	if err != nil {
   166  		return err
   167  	}
   168  
   169  	bytes, err := ioutil.ReadAll(request.Body)
   170  	if err != nil {
   171  		return err
   172  	}
   173  	fullMethod, err := procedureNameToFullMethod(request.Procedure)
   174  	if err != nil {
   175  		return err
   176  	}
   177  	var callOptions []grpc.CallOption
   178  	if responseMD != nil {
   179  		callOptions = []grpc.CallOption{grpc.Trailer(responseMD)}
   180  	}
   181  	if o.options.compressor != "" {
   182  		callOptions = append(callOptions, grpc.UseCompressor(o.options.compressor))
   183  	}
   184  	apiPeer, onFinish, err := o.peerChooser.Choose(ctx, request)
   185  	if err != nil {
   186  		return err
   187  	}
   188  	defer func() { onFinish(retErr) }()
   189  	grpcPeer, ok := apiPeer.(*grpcPeer)
   190  	if !ok {
   191  		return peer.ErrInvalidPeerConversion{
   192  			Peer:         apiPeer,
   193  			ExpectedType: "*grpcPeer",
   194  		}
   195  	}
   196  
   197  	tracer := o.t.options.tracer
   198  	createOpenTracingSpan := &transport.CreateOpenTracingSpan{
   199  		Tracer:        tracer,
   200  		TransportName: TransportName,
   201  		StartTime:     start,
   202  		ExtraTags:     yarpc.OpentracingTags,
   203  	}
   204  	ctx, span := createOpenTracingSpan.Do(ctx, request)
   205  	defer span.Finish()
   206  
   207  	if err := tracer.Inject(span.Context(), opentracing.HTTPHeaders, mdReadWriter(md)); err != nil {
   208  		return err
   209  	}
   210  
   211  	err = transport.UpdateSpanWithErr(
   212  		span,
   213  		grpcPeer.clientConn.Invoke(
   214  			metadata.NewOutgoingContext(ctx, md),
   215  			fullMethod,
   216  			bytes,
   217  			responseBody,
   218  			callOptions...,
   219  		),
   220  	)
   221  	if err != nil {
   222  		return invokeErrorToYARPCError(err, *responseMD)
   223  	}
   224  	// Service name match validation, return yarpcerrors.CodeInternal error if not match
   225  	if match, resSvcName := checkServiceMatch(request.Service, *responseMD); !match {
   226  		// If service doesn't match => we got response => span must not be nil
   227  		return transport.UpdateSpanWithErr(span, yarpcerrors.InternalErrorf("service name sent from the request "+
   228  			"does not match the service name received in the response: sent %q, got: %q", request.Service, resSvcName))
   229  	}
   230  	return nil
   231  }
   232  
   233  func metadataToIsApplicationError(responseMD metadata.MD) bool {
   234  	if responseMD == nil {
   235  		return false
   236  	}
   237  	value, ok := responseMD[ApplicationErrorHeader]
   238  	return ok && len(value) > 0 && len(value[0]) > 0
   239  }
   240  
   241  func invokeErrorToYARPCError(err error, responseMD metadata.MD) error {
   242  	if err == nil {
   243  		return nil
   244  	}
   245  	if yarpcerrors.IsStatus(err) {
   246  		return err
   247  	}
   248  	status, ok := status.FromError(err)
   249  	// if not a yarpc error or grpc error, just return a wrapped error
   250  	if !ok {
   251  		return yarpcerrors.FromError(err)
   252  	}
   253  	code, ok := grpcerrorcodes.GRPCCodeToYARPCCode[status.Code()]
   254  	if !ok {
   255  		code = yarpcerrors.CodeUnknown
   256  	}
   257  	var name string
   258  	if responseMD != nil {
   259  		value, ok := responseMD[ErrorNameHeader]
   260  		// TODO: what to do if the length is > 1?
   261  		if ok && len(value) == 1 {
   262  			name = value[0]
   263  		}
   264  	}
   265  	message := status.Message()
   266  	// we put the name as a prefix for grpc compatibility
   267  	// if there was no message, the message will be the name, so we leave it as the message
   268  	if name != "" && message != "" && message != name {
   269  		message = strings.TrimPrefix(message, name+": ")
   270  	} else if name != "" && message == name {
   271  		message = ""
   272  	}
   273  
   274  	yarpcErr := intyarpcerrors.NewWithNamef(code, name, message)
   275  	if details, err := marshalError(status); err != nil {
   276  		return err
   277  	} else if details != nil {
   278  		yarpcErr = yarpcErr.WithDetails(details)
   279  	}
   280  	return yarpcErr
   281  }
   282  
   283  // CallStream implements transport.StreamOutbound#CallStream.
   284  func (o *Outbound) CallStream(ctx context.Context, request *transport.StreamRequest) (*transport.ClientStream, error) {
   285  	if err := o.once.WaitUntilRunning(ctx); err != nil {
   286  		return nil, err
   287  	}
   288  	return o.stream(ctx, request, time.Now())
   289  }
   290  
   291  func (o *Outbound) stream(
   292  	ctx context.Context,
   293  	req *transport.StreamRequest,
   294  	start time.Time,
   295  ) (_ *transport.ClientStream, err error) {
   296  	if req.Meta == nil {
   297  		return nil, yarpcerrors.InvalidArgumentErrorf("stream request requires a request metadata")
   298  	}
   299  	treq := req.Meta.ToRequest()
   300  	if err := validateRequest(treq); err != nil {
   301  		return nil, err
   302  	}
   303  
   304  	md, err := transportRequestToMetadata(treq)
   305  	if err != nil {
   306  		return nil, err
   307  	}
   308  
   309  	fullMethod, err := procedureNameToFullMethod(req.Meta.Procedure)
   310  	if err != nil {
   311  		return nil, err
   312  	}
   313  
   314  	apiPeer, onFinish, err := o.peerChooser.Choose(ctx, treq)
   315  	if err != nil {
   316  		return nil, err
   317  	}
   318  
   319  	grpcPeer, ok := apiPeer.(*grpcPeer)
   320  	if !ok {
   321  		err := peer.ErrInvalidPeerConversion{
   322  			Peer:         apiPeer,
   323  			ExpectedType: "*grpcPeer",
   324  		}
   325  		onFinish(err)
   326  		return nil, err
   327  	}
   328  
   329  	tracer := o.t.options.tracer
   330  	createOpenTracingSpan := &transport.CreateOpenTracingSpan{
   331  		Tracer:        tracer,
   332  		TransportName: TransportName,
   333  		StartTime:     start,
   334  		ExtraTags:     yarpc.OpentracingTags,
   335  	}
   336  	_, span := createOpenTracingSpan.Do(ctx, treq)
   337  
   338  	if err := tracer.Inject(span.Context(), opentracing.HTTPHeaders, mdReadWriter(md)); err != nil {
   339  		span.Finish()
   340  		onFinish(err)
   341  		return nil, err
   342  	}
   343  
   344  	streamCtx := metadata.NewOutgoingContext(ctx, md)
   345  	clientStream, err := grpcPeer.clientConn.NewStream(
   346  		streamCtx,
   347  		&grpc.StreamDesc{
   348  			ClientStreams: true,
   349  			ServerStreams: true,
   350  		},
   351  		fullMethod,
   352  	)
   353  	if err != nil {
   354  		span.Finish()
   355  		onFinish(err)
   356  		return nil, err
   357  	}
   358  	stream := newClientStream(streamCtx, req, clientStream, span, onFinish)
   359  	tClientStream, err := transport.NewClientStream(stream)
   360  	if err != nil {
   361  		onFinish(err)
   362  		span.Finish()
   363  		return nil, err
   364  	}
   365  	return tClientStream, nil
   366  }
   367  
   368  // Introspect implements introspection.IntrospectableOutbound interface.
   369  func (o *Outbound) Introspect() introspection.OutboundStatus {
   370  	state := "Stopped"
   371  	if o.IsRunning() {
   372  		state = "Running"
   373  	}
   374  	var chooser introspection.ChooserStatus
   375  	if i, ok := o.peerChooser.(introspection.IntrospectableChooser); ok {
   376  		chooser = i.Introspect()
   377  	} else {
   378  		chooser = introspection.ChooserStatus{
   379  			Name: "Introspection not available",
   380  		}
   381  	}
   382  	return introspection.OutboundStatus{
   383  		Transport: TransportName,
   384  		State:     state,
   385  		Chooser:   chooser,
   386  	}
   387  }
   388  
   389  // Only does verification when there is a response service header key
   390  func checkServiceMatch(reqSvcName string, responseMD metadata.MD) (bool, string) {
   391  	if resSvcName, ok := responseMD[ServiceHeader]; ok {
   392  		return reqSvcName == resSvcName[0], resSvcName[0]
   393  	}
   394  	return true, ""
   395  }