| // Copyright 2015 go-swagger maintainers |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package middleware |
| |
| import ( |
| "fmt" |
| "net/http" |
| fpath "path" |
| "regexp" |
| "strings" |
| |
| "github.com/go-openapi/runtime/security" |
| |
| "github.com/go-openapi/analysis" |
| "github.com/go-openapi/errors" |
| "github.com/go-openapi/loads" |
| "github.com/go-openapi/runtime" |
| "github.com/go-openapi/runtime/middleware/denco" |
| "github.com/go-openapi/spec" |
| "github.com/go-openapi/strfmt" |
| ) |
| |
| // RouteParam is a object to capture route params in a framework agnostic way. |
| // implementations of the muxer should use these route params to communicate with the |
| // swagger framework |
| type RouteParam struct { |
| Name string |
| Value string |
| } |
| |
| // RouteParams the collection of route params |
| type RouteParams []RouteParam |
| |
| // Get gets the value for the route param for the specified key |
| func (r RouteParams) Get(name string) string { |
| vv, _, _ := r.GetOK(name) |
| if len(vv) > 0 { |
| return vv[len(vv)-1] |
| } |
| return "" |
| } |
| |
| // GetOK gets the value but also returns booleans to indicate if a key or value |
| // is present. This aids in validation and satisfies an interface in use there |
| // |
| // The returned values are: data, has key, has value |
| func (r RouteParams) GetOK(name string) ([]string, bool, bool) { |
| for _, p := range r { |
| if p.Name == name { |
| return []string{p.Value}, true, p.Value != "" |
| } |
| } |
| return nil, false, false |
| } |
| |
| // NewRouter creates a new context aware router middleware |
| func NewRouter(ctx *Context, next http.Handler) http.Handler { |
| if ctx.router == nil { |
| ctx.router = DefaultRouter(ctx.spec, ctx.api) |
| } |
| |
| return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { |
| if _, rCtx, ok := ctx.RouteInfo(r); ok { |
| next.ServeHTTP(rw, rCtx) |
| return |
| } |
| |
| // Not found, check if it exists in the other methods first |
| if others := ctx.AllowedMethods(r); len(others) > 0 { |
| ctx.Respond(rw, r, ctx.analyzer.RequiredProduces(), nil, errors.MethodNotAllowed(r.Method, others)) |
| return |
| } |
| |
| ctx.Respond(rw, r, ctx.analyzer.RequiredProduces(), nil, errors.NotFound("path %s was not found", r.URL.EscapedPath())) |
| }) |
| } |
| |
| // RoutableAPI represents an interface for things that can serve |
| // as a provider of implementations for the swagger router |
| type RoutableAPI interface { |
| HandlerFor(string, string) (http.Handler, bool) |
| ServeErrorFor(string) func(http.ResponseWriter, *http.Request, error) |
| ConsumersFor([]string) map[string]runtime.Consumer |
| ProducersFor([]string) map[string]runtime.Producer |
| AuthenticatorsFor(map[string]spec.SecurityScheme) map[string]runtime.Authenticator |
| Authorizer() runtime.Authorizer |
| Formats() strfmt.Registry |
| DefaultProduces() string |
| DefaultConsumes() string |
| } |
| |
| // Router represents a swagger aware router |
| type Router interface { |
| Lookup(method, path string) (*MatchedRoute, bool) |
| OtherMethods(method, path string) []string |
| } |
| |
| type defaultRouteBuilder struct { |
| spec *loads.Document |
| analyzer *analysis.Spec |
| api RoutableAPI |
| records map[string][]denco.Record |
| } |
| |
| type defaultRouter struct { |
| spec *loads.Document |
| routers map[string]*denco.Router |
| } |
| |
| func newDefaultRouteBuilder(spec *loads.Document, api RoutableAPI) *defaultRouteBuilder { |
| return &defaultRouteBuilder{ |
| spec: spec, |
| analyzer: analysis.New(spec.Spec()), |
| api: api, |
| records: make(map[string][]denco.Record), |
| } |
| } |
| |
| // DefaultRouter creates a default implemenation of the router |
| func DefaultRouter(spec *loads.Document, api RoutableAPI) Router { |
| builder := newDefaultRouteBuilder(spec, api) |
| if spec != nil { |
| for method, paths := range builder.analyzer.Operations() { |
| for path, operation := range paths { |
| fp := fpath.Join(spec.BasePath(), path) |
| debugLog("adding route %s %s %q", method, fp, operation.ID) |
| builder.AddRoute(method, fp, operation) |
| } |
| } |
| } |
| return builder.Build() |
| } |
| |
| // RouteAuthenticator is an authenticator that can compose several authenticators together. |
| // It also knows when it contains an authenticator that allows for anonymous pass through. |
| // Contains a group of 1 or more authenticators that have a logical AND relationship |
| type RouteAuthenticator struct { |
| Authenticator map[string]runtime.Authenticator |
| Schemes []string |
| Scopes map[string][]string |
| allScopes []string |
| commonScopes []string |
| allowAnonymous bool |
| } |
| |
| func (ra *RouteAuthenticator) AllowsAnonymous() bool { |
| return ra.allowAnonymous |
| } |
| |
| // AllScopes returns a list of unique scopes that is the combination |
| // of all the scopes in the requirements |
| func (ra *RouteAuthenticator) AllScopes() []string { |
| return ra.allScopes |
| } |
| |
| // CommonScopes returns a list of unique scopes that are common in all the |
| // scopes in the requirements |
| func (ra *RouteAuthenticator) CommonScopes() []string { |
| return ra.commonScopes |
| } |
| |
| // Authenticate Authenticator interface implementation |
| func (ra *RouteAuthenticator) Authenticate(req *http.Request, route *MatchedRoute) (bool, interface{}, error) { |
| if ra.allowAnonymous { |
| route.Authenticator = ra |
| return true, nil, nil |
| } |
| // iterate in proper order |
| var lastResult interface{} |
| for _, scheme := range ra.Schemes { |
| if authenticator, ok := ra.Authenticator[scheme]; ok { |
| applies, princ, err := authenticator.Authenticate(&security.ScopedAuthRequest{ |
| Request: req, |
| RequiredScopes: ra.Scopes[scheme], |
| }) |
| if !applies { |
| return false, nil, nil |
| } |
| |
| if err != nil { |
| route.Authenticator = ra |
| return true, nil, err |
| } |
| lastResult = princ |
| } |
| } |
| route.Authenticator = ra |
| return true, lastResult, nil |
| } |
| |
| func stringSliceUnion(slices ...[]string) []string { |
| unique := make(map[string]struct{}) |
| var result []string |
| for _, slice := range slices { |
| for _, entry := range slice { |
| if _, ok := unique[entry]; ok { |
| continue |
| } |
| unique[entry] = struct{}{} |
| result = append(result, entry) |
| } |
| } |
| return result |
| } |
| |
| func stringSliceIntersection(slices ...[]string) []string { |
| unique := make(map[string]int) |
| var intersection []string |
| |
| total := len(slices) |
| var emptyCnt int |
| for _, slice := range slices { |
| if len(slice) == 0 { |
| emptyCnt++ |
| continue |
| } |
| |
| for _, entry := range slice { |
| unique[entry]++ |
| if unique[entry] == total-emptyCnt { // this entry appeared in all the non-empty slices |
| intersection = append(intersection, entry) |
| } |
| } |
| } |
| |
| return intersection |
| } |
| |
| // RouteAuthenticators represents a group of authenticators that represent a logical OR |
| type RouteAuthenticators []RouteAuthenticator |
| |
| // AllowsAnonymous returns true when there is an authenticator that means optional auth |
| func (ras RouteAuthenticators) AllowsAnonymous() bool { |
| for _, ra := range ras { |
| if ra.AllowsAnonymous() { |
| return true |
| } |
| } |
| return false |
| } |
| |
| // Authenticate method implemention so this collection can be used as authenticator |
| func (ras RouteAuthenticators) Authenticate(req *http.Request, route *MatchedRoute) (bool, interface{}, error) { |
| var lastError error |
| var allowsAnon bool |
| var anonAuth RouteAuthenticator |
| |
| for _, ra := range ras { |
| if ra.AllowsAnonymous() { |
| anonAuth = ra |
| allowsAnon = true |
| continue |
| } |
| applies, usr, err := ra.Authenticate(req, route) |
| if !applies || err != nil || usr == nil { |
| if err != nil { |
| lastError = err |
| } |
| continue |
| } |
| return applies, usr, nil |
| } |
| |
| if allowsAnon && lastError == nil { |
| route.Authenticator = &anonAuth |
| return true, nil, lastError |
| } |
| return lastError != nil, nil, lastError |
| } |
| |
| type routeEntry struct { |
| PathPattern string |
| BasePath string |
| Operation *spec.Operation |
| Consumes []string |
| Consumers map[string]runtime.Consumer |
| Produces []string |
| Producers map[string]runtime.Producer |
| Parameters map[string]spec.Parameter |
| Handler http.Handler |
| Formats strfmt.Registry |
| Binder *untypedRequestBinder |
| Authenticators RouteAuthenticators |
| Authorizer runtime.Authorizer |
| } |
| |
| // MatchedRoute represents the route that was matched in this request |
| type MatchedRoute struct { |
| routeEntry |
| Params RouteParams |
| Consumer runtime.Consumer |
| Producer runtime.Producer |
| Authenticator *RouteAuthenticator |
| } |
| |
| // HasAuth returns true when the route has a security requirement defined |
| func (m *MatchedRoute) HasAuth() bool { |
| return len(m.Authenticators) > 0 |
| } |
| |
| // NeedsAuth returns true when the request still |
| // needs to perform authentication |
| func (m *MatchedRoute) NeedsAuth() bool { |
| return m.HasAuth() && m.Authenticator == nil |
| } |
| |
| func (d *defaultRouter) Lookup(method, path string) (*MatchedRoute, bool) { |
| mth := strings.ToUpper(method) |
| debugLog("looking up route for %s %s", method, path) |
| if Debug { |
| if len(d.routers) == 0 { |
| debugLog("there are no known routers") |
| } |
| for meth := range d.routers { |
| debugLog("got a router for %s", meth) |
| } |
| } |
| if router, ok := d.routers[mth]; ok { |
| if m, rp, ok := router.Lookup(fpath.Clean(path)); ok && m != nil { |
| if entry, ok := m.(*routeEntry); ok { |
| debugLog("found a route for %s %s with %d parameters", method, path, len(entry.Parameters)) |
| var params RouteParams |
| for _, p := range rp { |
| v, err := pathUnescape(p.Value) |
| if err != nil { |
| debugLog("failed to escape %q: %v", p.Value, err) |
| v = p.Value |
| } |
| // a workaround to handle fragment/composing parameters until they are supported in denco router |
| // check if this parameter is a fragment within a path segment |
| if xpos := strings.Index(entry.PathPattern, fmt.Sprintf("{%s}", p.Name)) + len(p.Name) + 2; xpos < len(entry.PathPattern) && entry.PathPattern[xpos] != '/' { |
| // extract fragment parameters |
| ep := strings.Split(entry.PathPattern[xpos:], "/")[0] |
| pnames, pvalues := decodeCompositParams(p.Name, v, ep, nil, nil) |
| for i, pname := range pnames { |
| params = append(params, RouteParam{Name: pname, Value: pvalues[i]}) |
| } |
| } else { |
| // use the parameter directly |
| params = append(params, RouteParam{Name: p.Name, Value: v}) |
| } |
| } |
| return &MatchedRoute{routeEntry: *entry, Params: params}, true |
| } |
| } else { |
| debugLog("couldn't find a route by path for %s %s", method, path) |
| } |
| } else { |
| debugLog("couldn't find a route by method for %s %s", method, path) |
| } |
| return nil, false |
| } |
| |
| func (d *defaultRouter) OtherMethods(method, path string) []string { |
| mn := strings.ToUpper(method) |
| var methods []string |
| for k, v := range d.routers { |
| if k != mn { |
| if _, _, ok := v.Lookup(fpath.Clean(path)); ok { |
| methods = append(methods, k) |
| continue |
| } |
| } |
| } |
| return methods |
| } |
| |
| // convert swagger parameters per path segment into a denco parameter as multiple parameters per segment are not supported in denco |
| var pathConverter = regexp.MustCompile(`{(.+?)}([^/]*)`) |
| |
| func decodeCompositParams(name string, value string, pattern string, names []string, values []string) ([]string, []string) { |
| pleft := strings.Index(pattern, "{") |
| names = append(names, name) |
| if pleft < 0 { |
| if strings.HasSuffix(value, pattern) { |
| values = append(values, value[:len(value)-len(pattern)]) |
| } else { |
| values = append(values, "") |
| } |
| } else { |
| toskip := pattern[:pleft] |
| pright := strings.Index(pattern, "}") |
| vright := strings.Index(value, toskip) |
| if vright >= 0 { |
| values = append(values, value[:vright]) |
| } else { |
| values = append(values, "") |
| value = "" |
| } |
| return decodeCompositParams(pattern[pleft+1:pright], value[vright+len(toskip):], pattern[pright+1:], names, values) |
| } |
| return names, values |
| } |
| |
| func (d *defaultRouteBuilder) AddRoute(method, path string, operation *spec.Operation) { |
| mn := strings.ToUpper(method) |
| |
| bp := fpath.Clean(d.spec.BasePath()) |
| if len(bp) > 0 && bp[len(bp)-1] == '/' { |
| bp = bp[:len(bp)-1] |
| } |
| |
| debugLog("operation: %#v", *operation) |
| if handler, ok := d.api.HandlerFor(method, strings.TrimPrefix(path, bp)); ok { |
| consumes := d.analyzer.ConsumesFor(operation) |
| produces := d.analyzer.ProducesFor(operation) |
| parameters := d.analyzer.ParamsFor(method, strings.TrimPrefix(path, bp)) |
| |
| record := denco.NewRecord(pathConverter.ReplaceAllString(path, ":$1"), &routeEntry{ |
| BasePath: bp, |
| PathPattern: path, |
| Operation: operation, |
| Handler: handler, |
| Consumes: consumes, |
| Produces: produces, |
| Consumers: d.api.ConsumersFor(normalizeOffers(consumes)), |
| Producers: d.api.ProducersFor(normalizeOffers(produces)), |
| Parameters: parameters, |
| Formats: d.api.Formats(), |
| Binder: newUntypedRequestBinder(parameters, d.spec.Spec(), d.api.Formats()), |
| Authenticators: d.buildAuthenticators(operation), |
| Authorizer: d.api.Authorizer(), |
| }) |
| d.records[mn] = append(d.records[mn], record) |
| } |
| } |
| |
| func (d *defaultRouteBuilder) buildAuthenticators(operation *spec.Operation) RouteAuthenticators { |
| requirements := d.analyzer.SecurityRequirementsFor(operation) |
| var auths []RouteAuthenticator |
| for _, reqs := range requirements { |
| var schemes []string |
| scopes := make(map[string][]string, len(reqs)) |
| var scopeSlices [][]string |
| for _, req := range reqs { |
| schemes = append(schemes, req.Name) |
| scopes[req.Name] = req.Scopes |
| scopeSlices = append(scopeSlices, req.Scopes) |
| } |
| |
| definitions := d.analyzer.SecurityDefinitionsForRequirements(reqs) |
| authenticators := d.api.AuthenticatorsFor(definitions) |
| auths = append(auths, RouteAuthenticator{ |
| Authenticator: authenticators, |
| Schemes: schemes, |
| Scopes: scopes, |
| allScopes: stringSliceUnion(scopeSlices...), |
| commonScopes: stringSliceIntersection(scopeSlices...), |
| allowAnonymous: len(reqs) == 1 && reqs[0].Name == "", |
| }) |
| } |
| return auths |
| } |
| |
| func (d *defaultRouteBuilder) Build() *defaultRouter { |
| routers := make(map[string]*denco.Router) |
| for method, records := range d.records { |
| router := denco.New() |
| _ = router.Build(records) |
| routers[method] = router |
| } |
| return &defaultRouter{ |
| spec: d.spec, |
| routers: routers, |
| } |
| } |