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 }