123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466 |
- package runtime
- import (
- "context"
- "errors"
- "fmt"
- "net/http"
- "net/textproto"
- "regexp"
- "strings"
- "github.com/grpc-ecosystem/grpc-gateway/v2/internal/httprule"
- "google.golang.org/grpc/codes"
- "google.golang.org/grpc/grpclog"
- "google.golang.org/grpc/health/grpc_health_v1"
- "google.golang.org/grpc/metadata"
- "google.golang.org/grpc/status"
- "google.golang.org/protobuf/proto"
- )
- // UnescapingMode defines the behavior of ServeMux when unescaping path parameters.
- type UnescapingMode int
- const (
- // UnescapingModeLegacy is the default V2 behavior, which escapes the entire
- // path string before doing any routing.
- UnescapingModeLegacy UnescapingMode = iota
- // UnescapingModeAllExceptReserved unescapes all path parameters except RFC 6570
- // reserved characters.
- UnescapingModeAllExceptReserved
- // UnescapingModeAllExceptSlash unescapes URL path parameters except path
- // separators, which will be left as "%2F".
- UnescapingModeAllExceptSlash
- // UnescapingModeAllCharacters unescapes all URL path parameters.
- UnescapingModeAllCharacters
- // UnescapingModeDefault is the default escaping type.
- // TODO(v3): default this to UnescapingModeAllExceptReserved per grpc-httpjson-transcoding's
- // reference implementation
- UnescapingModeDefault = UnescapingModeLegacy
- )
- var encodedPathSplitter = regexp.MustCompile("(/|%2F)")
- // A HandlerFunc handles a specific pair of path pattern and HTTP method.
- type HandlerFunc func(w http.ResponseWriter, r *http.Request, pathParams map[string]string)
- // ServeMux is a request multiplexer for grpc-gateway.
- // It matches http requests to patterns and invokes the corresponding handler.
- type ServeMux struct {
- // handlers maps HTTP method to a list of handlers.
- handlers map[string][]handler
- forwardResponseOptions []func(context.Context, http.ResponseWriter, proto.Message) error
- marshalers marshalerRegistry
- incomingHeaderMatcher HeaderMatcherFunc
- outgoingHeaderMatcher HeaderMatcherFunc
- metadataAnnotators []func(context.Context, *http.Request) metadata.MD
- errorHandler ErrorHandlerFunc
- streamErrorHandler StreamErrorHandlerFunc
- routingErrorHandler RoutingErrorHandlerFunc
- disablePathLengthFallback bool
- unescapingMode UnescapingMode
- }
- // ServeMuxOption is an option that can be given to a ServeMux on construction.
- type ServeMuxOption func(*ServeMux)
- // WithForwardResponseOption returns a ServeMuxOption representing the forwardResponseOption.
- //
- // forwardResponseOption is an option that will be called on the relevant context.Context,
- // http.ResponseWriter, and proto.Message before every forwarded response.
- //
- // The message may be nil in the case where just a header is being sent.
- func WithForwardResponseOption(forwardResponseOption func(context.Context, http.ResponseWriter, proto.Message) error) ServeMuxOption {
- return func(serveMux *ServeMux) {
- serveMux.forwardResponseOptions = append(serveMux.forwardResponseOptions, forwardResponseOption)
- }
- }
- // WithUnescapingMode sets the escaping type. See the definitions of UnescapingMode
- // for more information.
- func WithUnescapingMode(mode UnescapingMode) ServeMuxOption {
- return func(serveMux *ServeMux) {
- serveMux.unescapingMode = mode
- }
- }
- // SetQueryParameterParser sets the query parameter parser, used to populate message from query parameters.
- // Configuring this will mean the generated OpenAPI output is no longer correct, and it should be
- // done with careful consideration.
- func SetQueryParameterParser(queryParameterParser QueryParameterParser) ServeMuxOption {
- return func(serveMux *ServeMux) {
- currentQueryParser = queryParameterParser
- }
- }
- // HeaderMatcherFunc checks whether a header key should be forwarded to/from gRPC context.
- type HeaderMatcherFunc func(string) (string, bool)
- // DefaultHeaderMatcher is used to pass http request headers to/from gRPC context. This adds permanent HTTP header
- // keys (as specified by the IANA, e.g: Accept, Cookie, Host) to the gRPC metadata with the grpcgateway- prefix. If you want to know which headers are considered permanent, you can view the isPermanentHTTPHeader function.
- // HTTP headers that start with 'Grpc-Metadata-' are mapped to gRPC metadata after removing the prefix 'Grpc-Metadata-'.
- // Other headers are not added to the gRPC metadata.
- func DefaultHeaderMatcher(key string) (string, bool) {
- switch key = textproto.CanonicalMIMEHeaderKey(key); {
- case isPermanentHTTPHeader(key):
- return MetadataPrefix + key, true
- case strings.HasPrefix(key, MetadataHeaderPrefix):
- return key[len(MetadataHeaderPrefix):], true
- }
- return "", false
- }
- // WithIncomingHeaderMatcher returns a ServeMuxOption representing a headerMatcher for incoming request to gateway.
- //
- // This matcher will be called with each header in http.Request. If matcher returns true, that header will be
- // passed to gRPC context. To transform the header before passing to gRPC context, matcher should return modified header.
- func WithIncomingHeaderMatcher(fn HeaderMatcherFunc) ServeMuxOption {
- for _, header := range fn.matchedMalformedHeaders() {
- grpclog.Warningf("The configured forwarding filter would allow %q to be sent to the gRPC server, which will likely cause errors. See https://github.com/grpc/grpc-go/pull/4803#issuecomment-986093310 for more information.", header)
- }
- return func(mux *ServeMux) {
- mux.incomingHeaderMatcher = fn
- }
- }
- // matchedMalformedHeaders returns the malformed headers that would be forwarded to gRPC server.
- func (fn HeaderMatcherFunc) matchedMalformedHeaders() []string {
- if fn == nil {
- return nil
- }
- headers := make([]string, 0)
- for header := range malformedHTTPHeaders {
- out, accept := fn(header)
- if accept && isMalformedHTTPHeader(out) {
- headers = append(headers, out)
- }
- }
- return headers
- }
- // WithOutgoingHeaderMatcher returns a ServeMuxOption representing a headerMatcher for outgoing response from gateway.
- //
- // This matcher will be called with each header in response header metadata. If matcher returns true, that header will be
- // passed to http response returned from gateway. To transform the header before passing to response,
- // matcher should return modified header.
- func WithOutgoingHeaderMatcher(fn HeaderMatcherFunc) ServeMuxOption {
- return func(mux *ServeMux) {
- mux.outgoingHeaderMatcher = fn
- }
- }
- // WithMetadata returns a ServeMuxOption for passing metadata to a gRPC context.
- //
- // This can be used by services that need to read from http.Request and modify gRPC context. A common use case
- // is reading token from cookie and adding it in gRPC context.
- func WithMetadata(annotator func(context.Context, *http.Request) metadata.MD) ServeMuxOption {
- return func(serveMux *ServeMux) {
- serveMux.metadataAnnotators = append(serveMux.metadataAnnotators, annotator)
- }
- }
- // WithErrorHandler returns a ServeMuxOption for configuring a custom error handler.
- //
- // This can be used to configure a custom error response.
- func WithErrorHandler(fn ErrorHandlerFunc) ServeMuxOption {
- return func(serveMux *ServeMux) {
- serveMux.errorHandler = fn
- }
- }
- // WithStreamErrorHandler returns a ServeMuxOption that will use the given custom stream
- // error handler, which allows for customizing the error trailer for server-streaming
- // calls.
- //
- // For stream errors that occur before any response has been written, the mux's
- // ErrorHandler will be invoked. However, once data has been written, the errors must
- // be handled differently: they must be included in the response body. The response body's
- // final message will include the error details returned by the stream error handler.
- func WithStreamErrorHandler(fn StreamErrorHandlerFunc) ServeMuxOption {
- return func(serveMux *ServeMux) {
- serveMux.streamErrorHandler = fn
- }
- }
- // WithRoutingErrorHandler returns a ServeMuxOption for configuring a custom error handler to handle http routing errors.
- //
- // Method called for errors which can happen before gRPC route selected or executed.
- // The following error codes: StatusMethodNotAllowed StatusNotFound StatusBadRequest
- func WithRoutingErrorHandler(fn RoutingErrorHandlerFunc) ServeMuxOption {
- return func(serveMux *ServeMux) {
- serveMux.routingErrorHandler = fn
- }
- }
- // WithDisablePathLengthFallback returns a ServeMuxOption for disable path length fallback.
- func WithDisablePathLengthFallback() ServeMuxOption {
- return func(serveMux *ServeMux) {
- serveMux.disablePathLengthFallback = true
- }
- }
- // WithHealthEndpointAt returns a ServeMuxOption that will add an endpoint to the created ServeMux at the path specified by endpointPath.
- // When called the handler will forward the request to the upstream grpc service health check (defined in the
- // gRPC Health Checking Protocol).
- //
- // See here https://grpc-ecosystem.github.io/grpc-gateway/docs/operations/health_check/ for more information on how
- // to setup the protocol in the grpc server.
- //
- // If you define a service as query parameter, this will also be forwarded as service in the HealthCheckRequest.
- func WithHealthEndpointAt(healthCheckClient grpc_health_v1.HealthClient, endpointPath string) ServeMuxOption {
- return func(s *ServeMux) {
- // error can be ignored since pattern is definitely valid
- _ = s.HandlePath(
- http.MethodGet, endpointPath, func(w http.ResponseWriter, r *http.Request, _ map[string]string,
- ) {
- _, outboundMarshaler := MarshalerForRequest(s, r)
- resp, err := healthCheckClient.Check(r.Context(), &grpc_health_v1.HealthCheckRequest{
- Service: r.URL.Query().Get("service"),
- })
- if err != nil {
- s.errorHandler(r.Context(), s, outboundMarshaler, w, r, err)
- return
- }
- w.Header().Set("Content-Type", "application/json")
- if resp.GetStatus() != grpc_health_v1.HealthCheckResponse_SERVING {
- switch resp.GetStatus() {
- case grpc_health_v1.HealthCheckResponse_NOT_SERVING, grpc_health_v1.HealthCheckResponse_UNKNOWN:
- err = status.Error(codes.Unavailable, resp.String())
- case grpc_health_v1.HealthCheckResponse_SERVICE_UNKNOWN:
- err = status.Error(codes.NotFound, resp.String())
- }
- s.errorHandler(r.Context(), s, outboundMarshaler, w, r, err)
- return
- }
- _ = outboundMarshaler.NewEncoder(w).Encode(resp)
- })
- }
- }
- // WithHealthzEndpoint returns a ServeMuxOption that will add a /healthz endpoint to the created ServeMux.
- //
- // See WithHealthEndpointAt for the general implementation.
- func WithHealthzEndpoint(healthCheckClient grpc_health_v1.HealthClient) ServeMuxOption {
- return WithHealthEndpointAt(healthCheckClient, "/healthz")
- }
- // NewServeMux returns a new ServeMux whose internal mapping is empty.
- func NewServeMux(opts ...ServeMuxOption) *ServeMux {
- serveMux := &ServeMux{
- handlers: make(map[string][]handler),
- forwardResponseOptions: make([]func(context.Context, http.ResponseWriter, proto.Message) error, 0),
- marshalers: makeMarshalerMIMERegistry(),
- errorHandler: DefaultHTTPErrorHandler,
- streamErrorHandler: DefaultStreamErrorHandler,
- routingErrorHandler: DefaultRoutingErrorHandler,
- unescapingMode: UnescapingModeDefault,
- }
- for _, opt := range opts {
- opt(serveMux)
- }
- if serveMux.incomingHeaderMatcher == nil {
- serveMux.incomingHeaderMatcher = DefaultHeaderMatcher
- }
- if serveMux.outgoingHeaderMatcher == nil {
- serveMux.outgoingHeaderMatcher = func(key string) (string, bool) {
- return fmt.Sprintf("%s%s", MetadataHeaderPrefix, key), true
- }
- }
- return serveMux
- }
- // Handle associates "h" to the pair of HTTP method and path pattern.
- func (s *ServeMux) Handle(meth string, pat Pattern, h HandlerFunc) {
- s.handlers[meth] = append([]handler{{pat: pat, h: h}}, s.handlers[meth]...)
- }
- // HandlePath allows users to configure custom path handlers.
- // refer: https://grpc-ecosystem.github.io/grpc-gateway/docs/operations/inject_router/
- func (s *ServeMux) HandlePath(meth string, pathPattern string, h HandlerFunc) error {
- compiler, err := httprule.Parse(pathPattern)
- if err != nil {
- return fmt.Errorf("parsing path pattern: %w", err)
- }
- tp := compiler.Compile()
- pattern, err := NewPattern(tp.Version, tp.OpCodes, tp.Pool, tp.Verb)
- if err != nil {
- return fmt.Errorf("creating new pattern: %w", err)
- }
- s.Handle(meth, pattern, h)
- return nil
- }
- // ServeHTTP dispatches the request to the first handler whose pattern matches to r.Method and r.URL.Path.
- func (s *ServeMux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- ctx := r.Context()
- path := r.URL.Path
- if !strings.HasPrefix(path, "/") {
- _, outboundMarshaler := MarshalerForRequest(s, r)
- s.routingErrorHandler(ctx, s, outboundMarshaler, w, r, http.StatusBadRequest)
- return
- }
- // TODO(v3): remove UnescapingModeLegacy
- if s.unescapingMode != UnescapingModeLegacy && r.URL.RawPath != "" {
- path = r.URL.RawPath
- }
- if override := r.Header.Get("X-HTTP-Method-Override"); override != "" && s.isPathLengthFallback(r) {
- r.Method = strings.ToUpper(override)
- if err := r.ParseForm(); err != nil {
- _, outboundMarshaler := MarshalerForRequest(s, r)
- sterr := status.Error(codes.InvalidArgument, err.Error())
- s.errorHandler(ctx, s, outboundMarshaler, w, r, sterr)
- return
- }
- }
- var pathComponents []string
- // since in UnescapeModeLegacy, the URL will already have been fully unescaped, if we also split on "%2F"
- // in this escaping mode we would be double unescaping but in UnescapingModeAllCharacters, we still do as the
- // path is the RawPath (i.e. unescaped). That does mean that the behavior of this function will change its default
- // behavior when the UnescapingModeDefault gets changed from UnescapingModeLegacy to UnescapingModeAllExceptReserved
- if s.unescapingMode == UnescapingModeAllCharacters {
- pathComponents = encodedPathSplitter.Split(path[1:], -1)
- } else {
- pathComponents = strings.Split(path[1:], "/")
- }
- lastPathComponent := pathComponents[len(pathComponents)-1]
- for _, h := range s.handlers[r.Method] {
- // If the pattern has a verb, explicitly look for a suffix in the last
- // component that matches a colon plus the verb. This allows us to
- // handle some cases that otherwise can't be correctly handled by the
- // former LastIndex case, such as when the verb literal itself contains
- // a colon. This should work for all cases that have run through the
- // parser because we know what verb we're looking for, however, there
- // are still some cases that the parser itself cannot disambiguate. See
- // the comment there if interested.
- var verb string
- patVerb := h.pat.Verb()
- idx := -1
- if patVerb != "" && strings.HasSuffix(lastPathComponent, ":"+patVerb) {
- idx = len(lastPathComponent) - len(patVerb) - 1
- }
- if idx == 0 {
- _, outboundMarshaler := MarshalerForRequest(s, r)
- s.routingErrorHandler(ctx, s, outboundMarshaler, w, r, http.StatusNotFound)
- return
- }
- comps := make([]string, len(pathComponents))
- copy(comps, pathComponents)
- if idx > 0 {
- comps[len(comps)-1], verb = lastPathComponent[:idx], lastPathComponent[idx+1:]
- }
- pathParams, err := h.pat.MatchAndEscape(comps, verb, s.unescapingMode)
- if err != nil {
- var mse MalformedSequenceError
- if ok := errors.As(err, &mse); ok {
- _, outboundMarshaler := MarshalerForRequest(s, r)
- s.errorHandler(ctx, s, outboundMarshaler, w, r, &HTTPStatusError{
- HTTPStatus: http.StatusBadRequest,
- Err: mse,
- })
- }
- continue
- }
- h.h(w, r, pathParams)
- return
- }
- // if no handler has found for the request, lookup for other methods
- // to handle POST -> GET fallback if the request is subject to path
- // length fallback.
- // Note we are not eagerly checking the request here as we want to return the
- // right HTTP status code, and we need to process the fallback candidates in
- // order to do that.
- for m, handlers := range s.handlers {
- if m == r.Method {
- continue
- }
- for _, h := range handlers {
- var verb string
- patVerb := h.pat.Verb()
- idx := -1
- if patVerb != "" && strings.HasSuffix(lastPathComponent, ":"+patVerb) {
- idx = len(lastPathComponent) - len(patVerb) - 1
- }
- comps := make([]string, len(pathComponents))
- copy(comps, pathComponents)
- if idx > 0 {
- comps[len(comps)-1], verb = lastPathComponent[:idx], lastPathComponent[idx+1:]
- }
- pathParams, err := h.pat.MatchAndEscape(comps, verb, s.unescapingMode)
- if err != nil {
- var mse MalformedSequenceError
- if ok := errors.As(err, &mse); ok {
- _, outboundMarshaler := MarshalerForRequest(s, r)
- s.errorHandler(ctx, s, outboundMarshaler, w, r, &HTTPStatusError{
- HTTPStatus: http.StatusBadRequest,
- Err: mse,
- })
- }
- continue
- }
- // X-HTTP-Method-Override is optional. Always allow fallback to POST.
- // Also, only consider POST -> GET fallbacks, and avoid falling back to
- // potentially dangerous operations like DELETE.
- if s.isPathLengthFallback(r) && m == http.MethodGet {
- if err := r.ParseForm(); err != nil {
- _, outboundMarshaler := MarshalerForRequest(s, r)
- sterr := status.Error(codes.InvalidArgument, err.Error())
- s.errorHandler(ctx, s, outboundMarshaler, w, r, sterr)
- return
- }
- h.h(w, r, pathParams)
- return
- }
- _, outboundMarshaler := MarshalerForRequest(s, r)
- s.routingErrorHandler(ctx, s, outboundMarshaler, w, r, http.StatusMethodNotAllowed)
- return
- }
- }
- _, outboundMarshaler := MarshalerForRequest(s, r)
- s.routingErrorHandler(ctx, s, outboundMarshaler, w, r, http.StatusNotFound)
- }
- // GetForwardResponseOptions returns the ForwardResponseOptions associated with this ServeMux.
- func (s *ServeMux) GetForwardResponseOptions() []func(context.Context, http.ResponseWriter, proto.Message) error {
- return s.forwardResponseOptions
- }
- func (s *ServeMux) isPathLengthFallback(r *http.Request) bool {
- return !s.disablePathLengthFallback && r.Method == "POST" && r.Header.Get("Content-Type") == "application/x-www-form-urlencoded"
- }
- type handler struct {
- pat Pattern
- h HandlerFunc
- }
|