1
0
mirror of https://github.com/kataras/iris.git synced 2025-12-24 05:17:03 +00:00

General Improvements (UseRouter per Party, fix AutoTLS). Read HISTORY.md

relative to: https://github.com/kataras/iris/issues/1577 and https://github.com/kataras/iris/issues/1578
This commit is contained in:
Gerasimos (Makis) Maropoulos
2020-08-12 07:20:07 +03:00
parent da029d6f37
commit 0761bc35ee
15 changed files with 639 additions and 120 deletions

View File

@@ -153,6 +153,10 @@ func overlapRoute(r *Route, next *Route) {
// APIBuilder the visible API for constructing the router
// and child routers.
type APIBuilder struct {
// parent is the creator of this Party.
// It is nil on Root.
parent *APIBuilder // currently it's used only on UseRouter feature.
// the per-party APIBuilder with DI.
apiBuilderDI *APIContainer
@@ -184,7 +188,7 @@ type APIBuilder struct {
// the per-party relative path.
relativePath string
// allowMethods are filled with the `AllowMethods` func.
// allowMethods are filled with the `AllowMethods` method.
// They are used to create new routes
// per any party's (and its children) routes registered
// if the method "x" wasn't registered already via the `Handle` (and its extensions like `Get`, `Post`...).
@@ -194,23 +198,64 @@ type APIBuilder struct {
handlerExecutionRules ExecutionRules
// the per-party (and its children) route registration rule, see `SetRegisterRule`.
routeRegisterRule RouteRegisterRule
// routerFilters field is shared across Parties. Each Party registers
// one or more middlewares to run before the router itself using the `UseRouter` method.
// Each Party calls the shared filter (`partyMatcher`) that decides if its `UseRouter` handlers
// can be executed. By default it's based on party's static path and/or subdomain,
// it can be modified through an `Application.SetPartyMatcher` call
// once before or after routerFilters filled.
//
// The Key is the Party (instance of APIBuilder),
// value wraps the partyFilter + the handlers registered through `UseRouter`.
// See `GetRouterFilters` too.
routerFilters map[Party]*Filter
// partyMatcher field is shared across all Parties,
// can be modified through the Application level only.
//
// It defaults to the internal, simple, "defaultPartyMatcher".
// It applies when "routerFilters" are used.
partyMatcher PartyMatcherFunc
}
var _ Party = (*APIBuilder)(nil)
var _ RoutesProvider = (*APIBuilder)(nil) // passed to the default request handler (routerHandler)
var (
_ Party = (*APIBuilder)(nil)
_ PartyMatcher = (*APIBuilder)(nil)
_ RoutesProvider = (*APIBuilder)(nil) // passed to the default request handler (routerHandler)
)
// NewAPIBuilder creates & returns a new builder
// which is responsible to build the API and the router handler.
func NewAPIBuilder() *APIBuilder {
return &APIBuilder{
macros: macro.Defaults,
errors: errgroup.New("API Builder"),
relativePath: "/",
routes: new(repository),
apiBuilderDI: &APIContainer{Container: hero.New()},
parent: nil,
macros: macro.Defaults,
errors: errgroup.New("API Builder"),
relativePath: "/",
routes: new(repository),
apiBuilderDI: &APIContainer{Container: hero.New()},
routerFilters: make(map[Party]*Filter),
partyMatcher: defaultPartyMatcher,
}
}
// IsRoot reports whether this Party is the root Application's one.
// It will return false on all children Parties, no exception.
func (api *APIBuilder) IsRoot() bool {
return api.parent == nil
}
/* If requested:
// GetRoot returns the very first Party (the Application).
func (api *APIBuilder) GetRoot() *APIBuilder {
root := api.parent
for root != nil {
root = api.parent
}
return root
}*/
// ConfigureContainer accepts one or more functions that can be used
// to configure dependency injection features of this Party
// such as register dependency and register handlers that will automatically inject any valid dependency.
@@ -565,14 +610,18 @@ func removeDuplicates(elements []string) (result []string) {
return result
}
// Party groups routes which may have the same prefix and share same handlers,
// returns that new rich subrouter.
// Party returns a new child Party which inherites its
// parent's options and middlewares.
// If "relativePath" matches the parent's one then it returns the current Party.
// A Party groups routes which may have the same prefix or subdomain and share same middlewares.
//
// You can even declare a subdomain with relativePath as "mysub." or see `Subdomain`.
// To create a group of routes for subdomains
// use the `Subdomain` or `WildcardSubdomain` methods
// or pass a "relativePath" as "admin." or "*." respectfully.
func (api *APIBuilder) Party(relativePath string, handlers ...context.Handler) Party {
// if app.Party("/"), root party, then just add the middlewares
// and return itself.
if api.relativePath == "/" && (relativePath == "" || relativePath == "/") {
// if app.Party("/"), root party or app.Party("/user") == app.Party("/user")
// then just add the middlewares and return itself.
if relativePath == "" || api.relativePath == relativePath {
api.Use(handlers...)
return api
}
@@ -614,7 +663,10 @@ func (api *APIBuilder) Party(relativePath string, handlers ...context.Handler) P
beginGlobalHandlers: api.beginGlobalHandlers,
doneGlobalHandlers: api.doneGlobalHandlers,
errors: api.errors,
routerFilters: api.routerFilters, // shared.
partyMatcher: api.partyMatcher, // shared.
// per-party/children
parent: api,
middleware: middleware,
doneHandlers: api.doneHandlers[0:],
relativePath: fullpath,
@@ -758,6 +810,134 @@ func (api *APIBuilder) GetRouteReadOnlyByPath(tmplPath string) context.RouteRead
return r.ReadOnly
}
type (
// PartyMatcherFunc used to build a filter which decides
// if the given Party is responsible to fire its `UseRouter` handlers or not.
// Can be customized through `SetPartyMatcher` method. See `Match` method too.
PartyMatcherFunc func(Party, *context.Context) bool
// PartyMatcher decides if `UseRouter` handlers should be executed or not.
// A different interface becauwe we want to separate
// the Party's public API from `UseRouter` internals.
PartyMatcher interface {
Match(ctx *context.Context) bool
}
// Filter is a wraper for a Router Filter contains information
// for its Party's fullpath, subdomain the Party's
// matcher and the associated handlers to be executed before main router's request handler.
Filter struct {
Party Party // the Party itself
Matcher PartyMatcher // it's a Party, for freedom that can be changed through a custom matcher which accepts the same filter.
Subdomain string
Path string
Handlers context.Handlers
}
)
// SetPartyMatcher accepts a function which runs against
// a Party and should report whether its `UseRouter` handlers should be executed.
// PartyMatchers are run through parent to children.
// It modifies the default Party filter that decides
// which `UseRouter` middlewares to run before the Router,
// each one of those middlewares can skip `Context.Next` or call `Context.StopXXX`
// to stop the main router from searching for a route match.
// Can be called before or after `UseRouter`, it doesn't matter.
func (api *APIBuilder) SetPartyMatcher(matcherFunc PartyMatcherFunc) {
if matcherFunc == nil {
matcherFunc = defaultPartyMatcher
}
api.partyMatcher = matcherFunc
}
// Match reports whether the `UseRouter` handlers should be executed.
// Calls its parent's Match if possible.
// Implements the `PartyMatcher` interface.
func (api *APIBuilder) Match(ctx *context.Context) bool {
return api.partyMatcher(api, ctx)
}
func defaultPartyMatcher(p Party, ctx *context.Context) bool {
subdomain, path := splitSubdomainAndPath(p.GetRelPath())
staticPath := staticPath(path)
hosts := subdomain != ""
if p.IsRoot() {
// ALWAYS executed first when registered
// through an `Application.UseRouter` call.
return true
}
if hosts {
// Note(@kataras): do NOT try to implement something like party matcher for each party
// separately. We will introduce a new problem with subdomain inside a subdomain:
// they are not by prefix, so parenting calls will not help
// e.g. admin. and control.admin, control.admin is a sub of the admin.
if !canHandleSubdomain(ctx, subdomain) {
return false
}
}
// this is the longest static path.
return strings.HasPrefix(ctx.Path(), staticPath)
}
// GetRouterFilters returns the global router filters.
// Read `UseRouter` for more.
// The map can be altered before router built.
// The router internally prioritized them by the subdomains and
// longest static path.
// Implements the `RoutesProvider` interface.
func (api *APIBuilder) GetRouterFilters() map[Party]*Filter {
return api.routerFilters
}
// UseRouter upserts one or more handlers that will be fired
// right before the main router's request handler.
//
// Use this method to register handlers, that can ran
// independently of the incoming request's values,
// that they will be executed ALWAYS against ALL children incoming requests.
// Example of use-case: CORS.
//
// Note that because these are executed before the router itself
// the Context should not have access to the `GetCurrentRoute`
// as it is not decided yet which route is responsible to handle the incoming request.
// It's one level higher than the `WrapRouter`.
// The context SHOULD call its `Next` method in order to proceed to
// the next handler in the chain or the main request handler one.
func (api *APIBuilder) UseRouter(handlers ...context.Handler) {
if len(handlers) == 0 {
return
}
beginHandlers := context.Handlers(handlers)
// respect any execution rules (begin).
api.handlerExecutionRules.Begin.apply(&beginHandlers)
if f := api.routerFilters[api]; f != nil && len(f.Handlers) > 0 { // exists.
beginHandlers = context.UpsertHandlers(f.Handlers, beginHandlers) // remove dupls.
} else {
// Note(@kataras): we don't add the parent's filter handlers
// on `Party` method because we need to know if a `UseRouter` call exist
// before prepending the parent's ones and fill a new Filter on `routerFilters`,
// that key should NOT exist on a Party without `UseRouter` handlers (see router.go).
// That's the only reason we need the `parent` field.
if api.parent != nil {
// If it's not root, add the parent's handlers here.
if root, ok := api.routerFilters[api.parent]; ok {
beginHandlers = context.UpsertHandlers(root.Handlers, beginHandlers)
}
}
}
subdomain, path := splitSubdomainAndPath(api.relativePath)
api.routerFilters[api] = &Filter{
Matcher: api,
Subdomain: subdomain,
Path: path,
Handlers: beginHandlers,
}
}
// Use appends Handler(s) to the current Party's routes and child routes.
// If the current Party is the root, then it registers the middleware to all child Parties' routes too.
//
@@ -774,19 +954,7 @@ func (api *APIBuilder) Use(handlers ...context.Handler) {
// or on the basis of the middleware already existing,
// replace that existing middleware instead.
func (api *APIBuilder) UseOnce(handlers ...context.Handler) {
reg:
for _, handler := range handlers {
name := context.HandlerName(handler)
for i, registeredHandler := range api.middleware {
registeredName := context.HandlerName(registeredHandler)
if name == registeredName {
api.middleware[i] = handler // replace this handler with the new one.
continue reg // break and continue to the next handler.
}
}
api.middleware = append(api.middleware, handler) // or just insert it.
}
api.middleware = context.UpsertHandlers(api.middleware, handlers)
}
// UseGlobal registers handlers that should run at the very beginning.

View File

@@ -118,6 +118,10 @@ func (h *routerHandler) AddRoute(r *Route) error {
type RoutesProvider interface { // api builder
GetRoutes() []*Route
GetRoute(routeName string) *Route
// GetRouterFilters returns the app's router filters.
// Read `UseRouter` for more.
// The map can be altered before router built.
GetRouterFilters() map[Party]*Filter
}
func (h *routerHandler) Build(provider RoutesProvider) error {
@@ -318,12 +322,12 @@ func bindMultiParamTypesHandler(r *Route) {
r.topLink.beginHandlers = append(context.Handlers{decisionHandler}, r.topLink.beginHandlers...)
}
func (h *routerHandler) canHandleSubdomain(ctx *context.Context, subdomain string) bool {
func canHandleSubdomain(ctx *context.Context, subdomain string) bool {
if subdomain == "" {
return true
}
requestHost := ctx.Host()
requestHost := ctx.Request().URL.Host
if netutil.IsLoopbackSubdomain(requestHost) {
// this fixes a bug when listening on
// 127.0.0.1:8080 for example
@@ -349,7 +353,7 @@ func (h *routerHandler) canHandleSubdomain(ctx *context.Context, subdomain strin
return false
}
// continue to that, any subdomain is valid.
} else if !strings.HasPrefix(requestHost, subdomain) { // subdomain contains the dot.
} else if !strings.HasPrefix(requestHost, subdomain) { // subdomain contains the dot, e.g. "admin."
return false
}
@@ -396,7 +400,7 @@ func (h *routerHandler) HandleRequest(ctx *context.Context) {
continue
}
if h.hosts && !h.canHandleSubdomain(ctx, t.subdomain) {
if h.hosts && !canHandleSubdomain(ctx, t.subdomain) {
continue
}
@@ -499,7 +503,7 @@ func (h *routerHandler) FireErrorCode(ctx *context.Context) {
continue
}
if h.errorHosts && !h.canHandleSubdomain(ctx, t.subdomain) {
if h.errorHosts && !canHandleSubdomain(ctx, t.subdomain) {
continue
}

View File

@@ -13,6 +13,10 @@ import (
//
// Look the `APIBuilder` structure for its implementation.
type Party interface {
// IsRoot reports whether this Party is the root Application's one.
// It will return false on all children Parties, no exception.
IsRoot() bool
// ConfigureContainer accepts one or more functions that can be used
// to configure dependency injection features of this Party
// such as register dependency and register handlers that will automatically inject any valid dependency.
@@ -44,10 +48,14 @@ type Party interface {
// Look `OnErrorCode` too.
OnAnyErrorCode(handlers ...context.Handler) []*Route
// Party groups routes which may have the same prefix and share same handlers,
// returns that new rich subrouter.
// Party returns a new child Party which inherites its
// parent's options and middlewares.
// If "relativePath" matches the parent's one then it returns the current Party.
// A Party groups routes which may have the same prefix or subdomain and share same middlewares.
//
// You can even declare a subdomain with relativePath as "mysub." or see `Subdomain`.
// To create a group of routes for subdomains
// use the `Subdomain` or `WildcardSubdomain` methods
// or pass a "relativePath" as "admin." or "*." respectfully.
Party(relativePath string, middleware ...context.Handler) Party
// PartyFunc same as `Party`, groups routes that share a base path or/and same handlers.
// However this function accepts a function that receives this created Party instead.
@@ -73,6 +81,21 @@ type Party interface {
// So if app.Subdomain("admin").Subdomain("panel") then the result is: "panel.admin.".
Subdomain(subdomain string, middleware ...context.Handler) Party
// UseRouter upserts one or more handlers that will be fired
// right before the main router's request handler.
//
// Use this method to register handlers, that can ran
// independently of the incoming request's values,
// that they will be executed ALWAYS against ALL children incoming requests.
// Example of use-case: CORS.
//
// Note that because these are executed before the router itself
// the Context should not have access to the `GetCurrentRoute`
// as it is not decided yet which route is responsible to handle the incoming request.
// It's one level higher than the `WrapRouter`.
// The context SHOULD call its `Next` method in order to proceed to
// the next handler in the chain or the main request handler one.
UseRouter(handlers ...context.Handler)
// Use appends Handler(s) to the current Party's routes and child routes.
// If the current Party is the root, then it registers the middleware to all child Parties' routes too.
Use(middleware ...context.Handler)

View File

@@ -235,6 +235,14 @@ func splitSubdomainAndPath(fullUnparsedPath string) (subdomain string, path stri
subdomain = s[0:slashIdx]
}
if slashIdx == -1 {
// this will only happen when this function
// is called to Party's relative path (e.g. control.admin.),
// and not a route's one (the route's one always contains a slash).
// return all as subdomain and "/" as path.
return s, "/"
}
path = s[slashIdx:]
if !strings.Contains(path, "{") {
path = strings.ReplaceAll(path, "//", "/")

View File

@@ -3,6 +3,8 @@ package router
import (
"errors"
"net/http"
"sort"
"strings"
"sync"
"github.com/kataras/iris/v12/context"
@@ -19,7 +21,6 @@ import (
type Router struct {
mu sync.Mutex // for Downgrade, WrapRouter & BuildRouter,
preHandlers context.Handlers // run before requestHandler, as middleware, same way context's handlers run, see `UseRouter`.
requestHandler RequestHandler // build-accessible, can be changed to define a custom router or proxy, used on RefreshRouter too.
mainHandler http.HandlerFunc // init-accessible
wrapperFunc WrapperFunc
@@ -87,23 +88,78 @@ func (router *Router) FindClosestPaths(subdomain, searchPath string, n int) []st
return list
}
// UseRouter registers one or more handlers that are fired
// before the main router's request handler.
//
// Use this method to register handlers, that can ran
// independently of the incoming request's method and path values,
// that they will be executed ALWAYS against ALL incoming requests.
// Example of use-case: CORS.
//
// Note that because these are executed before the router itself
// the Context should not have access to the `GetCurrentRoute`
// as it is not decided yet which route is responsible to handle the incoming request.
// It's one level higher than the `WrapRouter`.
// The context SHOULD call its `Next` method in order to proceed to
// the next handler in the chain or the main request handler one.
// ExecutionRules are NOT applied here.
func (router *Router) UseRouter(handlers ...context.Handler) {
router.preHandlers = append(router.preHandlers, handlers...)
func (router *Router) buildMainHandlerWithFilters(routerFilters map[Party]*Filter, cPool *context.Pool, requestHandler RequestHandler) {
sortedFilters := make([]*Filter, 0, len(routerFilters))
// key was just there to enforce uniqueness on API level.
for _, f := range routerFilters {
sortedFilters = append(sortedFilters, f)
// append it as one handlers so execution rules are being respected in that step too.
f.Handlers = append(f.Handlers, func(ctx *context.Context) {
// set the handler index back to 0 so the route's handlers can be executed as expected.
ctx.HandlerIndex(0)
// execute the main request handler, this will fire the found route's handlers
// or if error the error code's associated handler.
router.requestHandler.HandleRequest(ctx)
})
}
sort.SliceStable(sortedFilters, func(i, j int) bool {
left, right := sortedFilters[i], sortedFilters[j]
var (
leftSubLen = len(left.Subdomain)
rightSubLen = len(right.Subdomain)
leftSlashLen = strings.Count(left.Path, "/")
rightSlashLen = strings.Count(right.Path, "/")
)
if leftSubLen == rightSubLen {
if leftSlashLen > rightSlashLen {
return true
}
}
if leftSubLen > rightSubLen {
return true
}
if leftSlashLen > rightSlashLen {
return true
}
if leftSlashLen == rightSlashLen {
if len(left.Path) > len(right.Path) {
return true
}
return false
}
return len(left.Path) > len(right.Path)
})
router.mainHandler = func(w http.ResponseWriter, r *http.Request) {
ctx := cPool.Acquire(w, r)
filterExecuted := false
for _, f := range sortedFilters {
// fmt.Printf("Sorted filter execution: [%s] [%s]\n", f.Subdomain, f.Path)
if f.Matcher.Match(ctx) {
// fmt.Printf("Matched [%s] and execute [%d] handlers [%s]\n\n", ctx.Path(), len(f.Handlers), context.HandlersNames(f.Handlers))
filterExecuted = true
// execute the final handlers chain.
ctx.Do(f.Handlers)
break
}
}
if !filterExecuted {
// If not at least one match filter found and executed,
// then just run the router.
router.requestHandler.HandleRequest(ctx)
}
cPool.Release(ctx)
}
}
// BuildRouter builds the router based on
@@ -149,22 +205,9 @@ func (router *Router) BuildRouter(cPool *context.Pool, requestHandler RequestHan
}
}
// the important
if len(router.preHandlers) > 0 {
handlers := append(router.preHandlers, func(ctx *context.Context) {
// set the handler index back to 0 so the route's handlers can be executed as exepcted.
ctx.HandlerIndex(0)
// execute the main request handler, this will fire the found route's handlers
// or if error the error code's associated handler.
router.requestHandler.HandleRequest(ctx)
})
router.mainHandler = func(w http.ResponseWriter, r *http.Request) {
ctx := cPool.Acquire(w, r)
// execute the final handlers chain.
ctx.Do(handlers)
cPool.Release(ctx)
}
// the important stuff.
if routerFilters := routesProvider.GetRouterFilters(); len(routerFilters) > 0 {
router.buildMainHandlerWithFilters(routerFilters, cPool, requestHandler)
} else {
router.mainHandler = func(w http.ResponseWriter, r *http.Request) {
ctx := cPool.Acquire(w, r)

View File

@@ -35,7 +35,13 @@ var (
secondUseHandler = writeHandler(secondUseResponse)
firstUseRouterResponse = "userouter1"
firstUseRouterHandler = writeHandler(firstUseRouterResponse)
// Use inline handler, no the `writeHandler`,
// because it will be overriden by `secondUseRouterHandler` otherwise,
// look `UseRouter:context.UpsertHandlers` for more.
firstUseRouterHandler = func(ctx iris.Context) {
ctx.WriteString(firstUseRouterResponse)
ctx.Next()
}
secondUseRouterResponse = "userouter2"
secondUseRouterHandler = writeHandler(secondUseRouterResponse)
@@ -178,3 +184,104 @@ func TestUseRouterStopExecution(t *testing.T) {
e = httptest.New(t, app)
e.GET("/").Expect().Status(iris.StatusForbidden).Body().Equal("err: custom error")
}
func TestUseRouterParentDisallow(t *testing.T) {
const expectedResponse = "no_userouter_allowed"
app := iris.New()
app.UseRouter(func(ctx iris.Context) {
ctx.WriteString("always")
ctx.Next()
})
app.Get("/index", func(ctx iris.Context) {
ctx.WriteString(expectedResponse)
})
app.SetPartyMatcher(func(p iris.Party, ctx iris.Context) bool {
// modifies the PartyMatcher to not match any UseRouter,
// tests should receive the handlers response alone.
return false
})
app.PartyFunc("/", func(p iris.Party) { // it's the same instance of app.
p.UseRouter(func(ctx iris.Context) {
ctx.WriteString("_2")
ctx.Next()
})
p.Get("/", func(ctx iris.Context) {
ctx.WriteString(expectedResponse)
})
})
app.PartyFunc("/user", func(p iris.Party) {
p.UseRouter(func(ctx iris.Context) {
ctx.WriteString("_3")
ctx.Next()
})
p.Get("/", func(ctx iris.Context) {
ctx.WriteString(expectedResponse)
})
})
e := httptest.New(t, app)
e.GET("/index").Expect().Status(iris.StatusOK).Body().Equal(expectedResponse)
e.GET("/").Expect().Status(iris.StatusOK).Body().Equal(expectedResponse)
e.GET("/user").Expect().Status(iris.StatusOK).Body().Equal(expectedResponse)
}
func TestUseRouterSubdomains(t *testing.T) {
app := iris.New()
app.UseRouter(func(ctx iris.Context) {
if ctx.Subdomain() == "old" {
ctx.Next() // call the router, do not write.
return
}
// if we write here, it will always give 200 OK,
// even on not registered routes, that's the point at the end,
// full control here when we need it.
ctx.WriteString("always_")
ctx.Next()
})
adminAPI := app.Subdomain("admin")
adminAPI.UseRouter(func(ctx iris.Context) {
ctx.WriteString("admin always_")
ctx.Next()
})
adminAPI.Get("/", func(ctx iris.Context) {
ctx.WriteString("admin")
})
adminControlAPI := adminAPI.Subdomain("control")
adminControlAPI.UseRouter(func(ctx iris.Context) {
ctx.WriteString("control admin always_")
ctx.Next()
})
adminControlAPI.Get("/", func(ctx iris.Context) {
ctx.WriteString("control admin")
})
oldAPI := app.Subdomain("old")
oldAPI.Get("/", func(ctx iris.Context) {
ctx.WriteString("chat")
})
e := httptest.New(t, app, httptest.URL("http://example.com"))
e.GET("/notfound").Expect().Status(iris.StatusOK).Body().Equal("always_")
e.GET("/").WithURL("http://admin.example.com").Expect().Status(iris.StatusOK).Body().
Equal("always_admin always_admin")
e.GET("/").WithURL("http://control.admin.example.com").Expect().Status(iris.StatusOK).Body().
Equal("always_admin always_control admin always_control admin")
// It has a route, and use router just proceeds to the router.
e.GET("/").WithURL("http://old.example.com").Expect().Status(iris.StatusOK).Body().
Equal("chat")
// this is not a registered path, should fire 404, the UseRouter does not write
// anything to the response writer, so the router has control over it.
e.GET("/notfound").WithURL("http://old.example.com").Expect().Status(iris.StatusNotFound).Body().
Equal("Not Found")
}