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 }