|
- // 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,
- }
- }
|