Платформа ЦРНП "Мирокод" для разработки проектов
https://git.mirocod.ru
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
478 lines
14 KiB
478 lines
14 KiB
// 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/spec" |
|
"github.com/go-openapi/strfmt" |
|
|
|
"github.com/go-openapi/runtime" |
|
"github.com/go-openapi/runtime/middleware/denco" |
|
) |
|
|
|
// 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, |
|
} |
|
}
|
|
|