From d44b69faed7538aeb4a05ec72ec664fbee7f5c04 Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Wed, 26 Aug 2020 06:57:36 +0300 Subject: [PATCH] Add Party.ResetRouterFilters relative to: https://github.com/kataras/iris/issues/1604#issuecomment-680410131 --- HISTORY.md | 2 +- core/router/api_builder.go | 72 ++++++++---- core/router/party.go | 5 + core/router/router.go | 4 +- core/router/router_handlers_order_test.go | 2 +- middleware/basicauth/basicauth_test.go | 131 ++++++++++++++++++++++ 6 files changed, 191 insertions(+), 25 deletions(-) create mode 100644 middleware/basicauth/basicauth_test.go diff --git a/HISTORY.md b/HISTORY.md index 8d080c15..dd7073ed 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -466,7 +466,7 @@ func main() { - `Party.UseError(...Handler)` - to register handlers to run before any http errors (e.g. before `OnErrorCode/OnAnyErrorCode` or default error codes when no handler is responsible to handle a specific http status code). -- `Party.UseRouter(...Handler)` - to register handlers before the main router, useful on handlers that should control whether the router itself should ran or not. Independently of the incoming request's method and path values. These handlers will be executed ALWAYS against ALL incoming matched requests. Example of use-case: CORS. +- `Party.UseRouter(...Handler) and Party.ResetRouterFilters()` - to register handlers before the main router, useful on handlers that should control whether the router itself should ran or not. Independently of the incoming request's method and path values. These handlers will be executed ALWAYS against ALL incoming matched requests. Example of use-case: CORS. - `*versioning.Group` type is a full `Party` now. diff --git a/core/router/api_builder.go b/core/router/api_builder.go index b55cf421..a5dc863d 100644 --- a/core/router/api_builder.go +++ b/core/router/api_builder.go @@ -158,7 +158,7 @@ type APIBuilder struct { logger *golog.Logger // parent is the creator of this Party. // It is nil on Root. - parent *APIBuilder // currently it's used only on UseRouter feature. + parent *APIBuilder // currently it's not used anywhere. // the per-party APIBuilder with DI. apiBuilderDI *APIContainer @@ -169,7 +169,8 @@ type APIBuilder struct { routes *repository // the per-party handlers, order - // of handlers registration matters. + // of handlers registration matters, + // inherited by children unless Reset is called. middleware context.Handlers middlewareErrorCode context.Handlers // the global middleware handlers, order of call doesn't matters, order @@ -197,6 +198,10 @@ type APIBuilder struct { // the per-party (and its children) route registration rule, see `SetRegisterRule`. routeRegisterRule RouteRegisterRule + // routerFilterHandlers holds a reference + // of the handlers used by the current and its parent Party's registered + // router filters. Inherited by children unless `Reset` (see `UseRouter`), + routerFilterHandlers context.Handlers // 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 @@ -665,13 +670,15 @@ func (api *APIBuilder) Party(relativePath string, handlers ...context.Handler) P routes: api.routes, beginGlobalHandlers: api.beginGlobalHandlers, doneGlobalHandlers: api.doneGlobalHandlers, - routerFilters: api.routerFilters, // shared. - partyMatcher: api.partyMatcher, // shared. + // per-party/children parent: api, middleware: middleware, middlewareErrorCode: context.JoinHandlers(api.middlewareErrorCode, context.Handlers{}), doneHandlers: api.doneHandlers[0:], + routerFilters: api.routerFilters, + routerFilterHandlers: api.routerFilterHandlers, + partyMatcher: api.partyMatcher, relativePath: fullpath, allowMethods: allowMethods, handlerExecutionRules: api.handlerExecutionRules, @@ -817,7 +824,7 @@ 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 + PartyMatcherFunc func(*context.Context, Party) 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. @@ -828,8 +835,8 @@ type ( // 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. + Matcher PartyMatcher // it's a Party, for freedom that can be changed through a custom matcher which accepts the same filter. + Skippers map[*APIBuilder]struct{} // skip execution on these builders ( see `Reset`) Subdomain string Path string Handlers context.Handlers @@ -855,10 +862,10 @@ func (api *APIBuilder) SetPartyMatcher(matcherFunc PartyMatcherFunc) { // Calls its parent's Match if possible. // Implements the `PartyMatcher` interface. func (api *APIBuilder) Match(ctx *context.Context) bool { - return api.partyMatcher(api, ctx) + return api.partyMatcher(ctx, api) } -func defaultPartyMatcher(p Party, ctx *context.Context) bool { +func defaultPartyMatcher(ctx *context.Context, p Party) bool { subdomain, path := splitSubdomainAndPath(p.GetRelPath()) staticPath := staticPath(path) hosts := subdomain != "" @@ -915,23 +922,16 @@ func (api *APIBuilder) UseRouter(handlers ...context.Handler) { beginHandlers := context.Handlers(handlers) // respect any execution rules (begin). api.handlerExecutionRules.Begin.apply(&beginHandlers) + beginHandlers = context.JoinHandlers(api.routerFilterHandlers, 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) - } - } } + // we are not using the parent field here, + // we need to have control over those values in order to be able to `Reset`. + api.routerFilterHandlers = beginHandlers + subdomain, path := splitSubdomainAndPath(api.relativePath) api.routerFilters[api] = &Filter{ Matcher: api, @@ -1033,13 +1033,43 @@ func (api *APIBuilder) DoneGlobal(handlers ...context.Handler) { func (api *APIBuilder) Reset() Party { api.middleware = api.middleware[0:0] api.middlewareErrorCode = api.middlewareErrorCode[0:0] + api.ResetRouterFilters() + api.doneHandlers = api.doneHandlers[0:0] api.handlerExecutionRules = ExecutionRules{} api.routeRegisterRule = RouteOverride + // keep container as it's. return api } +// ResetRouterFilters deactivates any pervious registered +// router filters and the parents ones for this Party. +// +// Returns this Party. +func (api *APIBuilder) ResetRouterFilters() Party { + api.routerFilterHandlers = api.routerFilterHandlers[0:0] + delete(api.routerFilters, api) + + if api.parent == nil { + // it's the root, stop, nothing else to do here. + return api + } + + // Set a filter with empty handlers, the router will find it, execute nothing + // and continue with the request handling. This works on Reset() and no UseRouter + // and with Reset().UseRouter. + subdomain, path := splitSubdomainAndPath(api.relativePath) + api.routerFilters[api] = &Filter{ + Matcher: api, + Handlers: nil, + Subdomain: subdomain, + Path: path, + } + + return api +} + // None registers an "offline" route // see context.ExecRoute(routeName) and // party.Routes().Online(handleResultRouteInfo, "GET") and diff --git a/core/router/party.go b/core/router/party.go index 10468e60..6bad7744 100644 --- a/core/router/party.go +++ b/core/router/party.go @@ -121,6 +121,11 @@ type Party interface { // // Returns this Party. Reset() Party + // ResetRouterFilters deactivates any pervious registered + // router filters and the parents ones for this Party. + // + // Returns this Party. + ResetRouterFilters() Party // AllowMethods will re-register the future routes that will be registered // via `Handle`, `Get`, `Post`, ... to the given "methods" on that Party and its children "Parties", diff --git a/core/router/router.go b/core/router/router.go index abdbe628..4d8fe38b 100644 --- a/core/router/router.go +++ b/core/router/router.go @@ -149,14 +149,14 @@ func (router *Router) buildMainHandlerWithFilters(routerFilters map[Party]*Filte ctx := cPool.Acquire(w, r) filterExecuted := false - for _, f := range sortedFilters { + for _, f := range sortedFilters { // from subdomain, largest path to shortest. // 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 + break // and break on first found. } } diff --git a/core/router/router_handlers_order_test.go b/core/router/router_handlers_order_test.go index 110993e8..1ecfc67a 100644 --- a/core/router/router_handlers_order_test.go +++ b/core/router/router_handlers_order_test.go @@ -198,7 +198,7 @@ func TestUseRouterParentDisallow(t *testing.T) { ctx.WriteString(expectedResponse) }) - app.SetPartyMatcher(func(p iris.Party, ctx iris.Context) bool { + app.SetPartyMatcher(func(ctx iris.Context, p iris.Party) bool { // modifies the PartyMatcher to not match any UseRouter, // tests should receive the handlers response alone. return false diff --git a/middleware/basicauth/basicauth_test.go b/middleware/basicauth/basicauth_test.go new file mode 100644 index 00000000..76406a51 --- /dev/null +++ b/middleware/basicauth/basicauth_test.go @@ -0,0 +1,131 @@ +// Package basicauth_tests performs black-box testing of the basicauth middleware. +// Note that, a secondary test is also available at: _examples/auth/basicauth/main_test.go +package basicauth_test + +import ( + "fmt" + "testing" + + "github.com/kataras/iris/v12" + "github.com/kataras/iris/v12/httptest" + "github.com/kataras/iris/v12/middleware/basicauth" +) + +func TestBasicAuthUseRouter(t *testing.T) { + app := iris.New() + users := map[string]string{ + "usr": "pss", + "admin": "admin", + } + + app.UseRouter(basicauth.Default(users)) + + app.Get("/", func(ctx iris.Context) { + username, _, _ := ctx.Request().BasicAuth() + ctx.Writef("Hello, %s!", username) + }) + + app.Subdomain("static").Get("/", func(ctx iris.Context) { + username, _, _ := ctx.Request().BasicAuth() + ctx.Writef("Static, %s", username) + }) + + resetWithUseRouter := app.Subdomain("reset_with_use_router").ResetRouterFilters() + resetWithUseRouter.UseRouter(func(ctx iris.Context) { + ctx.Record() + ctx.Writef("with use router\n") + ctx.Next() + }) + resetWithUseRouter.Get("/", func(ctx iris.Context) { + username, _, _ := ctx.Request().BasicAuth() + ctx.Writef("%s", username) // username should be empty. + }) + // ^ order of these should not matter. + app.Subdomain("reset").ResetRouterFilters().Get("/", func(ctx iris.Context) { + username, _, _ := ctx.Request().BasicAuth() + ctx.Writef("%s", username) // username should be empty. + }) + + e := httptest.New(t, app.Configure( + iris.WithFireMethodNotAllowed, + iris.WithResetOnFireErrorCode, + )) + + for username, password := range users { + // Test pass authentication and route found. + e.GET("/").WithBasicAuth(username, password).Expect(). + Status(httptest.StatusOK).Body().Equal(fmt.Sprintf("Hello, %s!", username)) + + // Test empty auth. + e.GET("/").Expect().Status(httptest.StatusUnauthorized).Body().Equal("Unauthorized") + // Test invalid auth. + e.GET("/").WithBasicAuth(username, "invalid_password").Expect(). + Status(httptest.StatusUnauthorized).Body().Equal("Unauthorized") + e.GET("/").WithBasicAuth("invaid_username", password).Expect(). + Status(httptest.StatusUnauthorized).Body().Equal("Unauthorized") + + // Test different method, it should pass the authentication (no stop on 401) + // but it doesn't fire the GET route, instead it gives 405. + e.POST("/").WithBasicAuth(username, password).Expect(). + Status(httptest.StatusMethodNotAllowed).Body().Equal("Method Not Allowed") + + // Test pass the authentication but route not found. + e.GET("/notfound").WithBasicAuth(username, password).Expect(). + Status(httptest.StatusNotFound).Body().Equal("Not Found") + + // Test empty auth. + e.GET("/notfound").Expect().Status(httptest.StatusUnauthorized).Body().Equal("Unauthorized") + // Test invalid auth. + e.GET("/notfound").WithBasicAuth(username, "invalid_password").Expect(). + Status(httptest.StatusUnauthorized).Body().Equal("Unauthorized") + e.GET("/notfound").WithBasicAuth("invaid_username", password).Expect(). + Status(httptest.StatusUnauthorized).Body().Equal("Unauthorized") + + // Test subdomain inherited. + sub := e.Builder(func(req *httptest.Request) { + req.WithURL("http://static.mydomain.com") + }) + + // Test pass and route found. + sub.GET("/").WithBasicAuth(username, password).Expect(). + Status(httptest.StatusOK).Body().Equal(fmt.Sprintf("Static, %s", username)) + + // Test empty auth. + sub.GET("/").Expect().Status(httptest.StatusUnauthorized) + // Test invalid auth. + sub.GET("/").WithBasicAuth(username, "invalid_password").Expect(). + Status(httptest.StatusUnauthorized).Body().Equal("Unauthorized") + sub.GET("/").WithBasicAuth("invaid_username", password).Expect(). + Status(httptest.StatusUnauthorized).Body().Equal("Unauthorized") + + // Test pass the authentication but route not found. + sub.GET("/notfound").WithBasicAuth(username, password).Expect(). + Status(httptest.StatusNotFound).Body().Equal("Not Found") + + // Test empty auth. + sub.GET("/notfound").Expect().Status(httptest.StatusUnauthorized).Body().Equal("Unauthorized") + // Test invalid auth. + sub.GET("/notfound").WithBasicAuth(username, "invalid_password").Expect(). + Status(httptest.StatusUnauthorized).Body().Equal("Unauthorized") + sub.GET("/notfound").WithBasicAuth("invaid_username", password).Expect(). + Status(httptest.StatusUnauthorized).Body().Equal("Unauthorized") + + // Test a reset-ed Party with a single one UseRouter + // which writes on matched routes and reset and send the error on errors. + // (all should pass without auth). + sub = e.Builder(func(req *httptest.Request) { + req.WithURL("http://reset_with_use_router.mydomain.com") + }) + sub.GET("/").Expect().Status(httptest.StatusOK).Body().Equal("with use router\n") + sub.POST("/").Expect().Status(httptest.StatusMethodNotAllowed).Body().Equal("Method Not Allowed") + sub.GET("/notfound").Expect().Status(httptest.StatusNotFound).Body().Equal("Not Found") + + // Test a reset-ed Party (all should pass without auth). + sub = e.Builder(func(req *httptest.Request) { + req.WithURL("http://reset.mydomain.com") + }) + sub.GET("/").Expect().Status(httptest.StatusOK).Body().Empty() + sub.POST("/").Expect().Status(httptest.StatusMethodNotAllowed).Body().Equal("Method Not Allowed") + sub.GET("/notfound").Expect().Status(httptest.StatusNotFound).Body().Equal("Not Found") + } +}