diff --git a/_examples/experimental-handlers/cors/simple/main.go b/_examples/experimental-handlers/cors/simple/main.go index 8f9cb1bf..611f664b 100644 --- a/_examples/experimental-handlers/cors/simple/main.go +++ b/_examples/experimental-handlers/cors/simple/main.go @@ -5,6 +5,7 @@ package main import ( "github.com/kataras/iris" + "github.com/kataras/iris/core/router" "github.com/iris-contrib/middleware/cors" ) @@ -14,6 +15,7 @@ func main() { app := iris.New() crs := cors.New(cors.Options{ AllowedOrigins: []string{"*"}, // allows everything, use that to change the hosts. + AllowedMethods: router.AllMethods[:], AllowCredentials: true, }) @@ -29,6 +31,12 @@ func main() { v1.Post("/send", func(ctx iris.Context) { ctx.WriteString("sent") }) + v1.Put("/send", func(ctx iris.Context) { + ctx.WriteString("updated") + }) + v1.Delete("/send", func(ctx iris.Context) { + ctx.WriteString("deleted") + }) } // or use that to wrap the entire router diff --git a/_examples/routing/fallback-handlers/main.go b/_examples/routing/fallback-handlers/main.go new file mode 100644 index 00000000..3c1e8e78 --- /dev/null +++ b/_examples/routing/fallback-handlers/main.go @@ -0,0 +1,29 @@ +package main + +import ( + "github.com/kataras/iris" +) + +func main() { + app := iris.New() + + // this works as expected now, + // will handle *all* expect DELETE requests, even if there is no routes + app.Get("/action/{p}", h) + + app.Run(iris.Addr(":8080"), ctx.Method(), ctx.Path(), iris.WithoutServerError(iris.ErrServerClosed)) +} + +func h(ctx iris.Context) { + ctx.Writef("[%s] %s : Parameter = `%s`", ctx.Params().Get("p")) +} + +func fallbackHandler(ctx iris.Context) { + if ctx.Method() == "DELETE" { + ctx.Next() + + return + } + + ctx.Writef("[%s] %s : From fallback handler", ctx.Method(), ctx.Path()) +} diff --git a/context/application.go b/context/application.go index 90738ce7..6ac8bd1f 100644 --- a/context/application.go +++ b/context/application.go @@ -48,4 +48,7 @@ type Application interface { // If a handler is not already registered, // then it creates & registers a new trivial handler on the-fly. FireErrorCode(ctx Context) + + // RouteExists checks if a route exists + RouteExists(method string, path string, ctx Context) bool } diff --git a/context/context.go b/context/context.go index dc3ecf80..1143aa2a 100644 --- a/context/context.go +++ b/context/context.go @@ -905,6 +905,9 @@ type Context interface { // It's for extreme use cases, 99% of the times will never be useful for you. Exec(method string, path string) + // RouteExists checks if a route exists + RouteExists(method string, path string) bool + // Application returns the iris app instance which belongs to this context. // Worth to notice that this function returns an interface // of the Application, which contains methods that are safe @@ -3130,6 +3133,11 @@ func (ctx *context) Exec(method string, path string) { } } +// RouteExists checks if a route exists +func (ctx *context) RouteExists(method string, path string) bool { + return ctx.Application().RouteExists(method, path, ctx) +} + // Application returns the iris app instance which belongs to this context. // Worth to notice that this function returns an interface // of the Application, which contains methods that are safe diff --git a/core/router/api_builder.go b/core/router/api_builder.go index e0cfc4d7..a300760c 100644 --- a/core/router/api_builder.go +++ b/core/router/api_builder.go @@ -273,7 +273,7 @@ func (api *APIBuilder) Party(relativePath string, handlers ...context.Handler) P // per-party/children middleware: middleware, doneHandlers: api.doneHandlers, - fallbackStack: api.fallbackStack, + fallbackStack: api.fallbackStack.Fork(), relativePath: fullpath, } } @@ -426,13 +426,19 @@ func (api *APIBuilder) DoneGlobal(handlers ...context.Handler) { api.doneGlobalHandlers = append(api.doneGlobalHandlers, handlers...) } -// Fallback appends Handler(s) to the current Party's fallback stack. +// Fallback appends Handler(s) to the current fallback stack. // Handler(s) is(are) called from Fallback stack when no route found and before sending NotFound status. // Therefore Handler(s) in Fallback stack could send another thing than NotFound status, // if `Context.Next()` method is not called. // Done & DoneGlobal Handlers are not called. func (api *APIBuilder) Fallback(middleware ...context.Handler) { - api.fallbackStack.add(middleware) + api.fallbackStack.Add(middleware) +} + +// FallBackStack returns Fallback stack, this is implementation of interface RoutesProvider +// that is used in Router building by the RequestHandler. +func (api *APIBuilder) GetFallBackStack() *FallbackStack { + return api.fallbackStack } // Reset removes all the begin and done handlers that may derived from the parent party via `Use` & `Done`, diff --git a/core/router/fallback_stack.go b/core/router/fallback_stack.go index 499e0896..1b8531f8 100644 --- a/core/router/fallback_stack.go +++ b/core/router/fallback_stack.go @@ -2,37 +2,71 @@ package router import ( "net/http" - "sync" "github.com/kataras/iris/context" ) +// FallbackStack is a stack (with LIFO calling order) for fallback handlers +// A fallback handler(s) is(are) called from Fallback stack +// when no route found and before sending NotFound status. +// Therefore Handler(s) in Fallback stack could send another thing than NotFound status, +// if `Context.Next()` method is not called. +// Done & DoneGlobal Handlers are not called. type FallbackStack struct { + parent *FallbackStack handlers context.Handlers - m sync.Mutex } -func (stk *FallbackStack) add(h context.Handlers) { - stk.m.Lock() - defer stk.m.Unlock() +// _size is a terminal recursive method for computing size the stack +func (stk *FallbackStack) _size(i int) int { + res := i + len(stk.handlers) + if stk.parent == nil { + return res + } + + return stk.parent._size(res) +} + +// populate is a recursive method for concatenating handlers to `list` parameter +func (stk *FallbackStack) populate(list context.Handlers) { + n := copy(list, stk.handlers) + + if stk.parent != nil { + stk.parent.populate(list[n:]) + } +} + +// Size gives the size of the full stack hierarchy +func (stk *FallbackStack) Size() int { + return stk._size(0) +} + +// Add appends handlers to the beginning of the stack to have a LIFO calling order +func (stk *FallbackStack) Add(h context.Handlers) { stk.handlers = append(stk.handlers, h...) copy(stk.handlers[len(h):], stk.handlers) copy(stk.handlers, h) } -func (stk *FallbackStack) list() context.Handlers { - res := make(context.Handlers, len(stk.handlers)) +// Fork make a new stack from this stack, and so create a stack child (leaf from a tree of stacks) +func (stk *FallbackStack) Fork() *FallbackStack { + return &FallbackStack{ + parent: stk, + } +} - stk.m.Lock() - defer stk.m.Unlock() - - copy(res, stk.handlers) +// List concatenate all handlers in stack hierarchy +func (stk *FallbackStack) List() context.Handlers { + res := make(context.Handlers, stk.Size()) + stk.populate(res) return res } +// NewFallbackStack create a new Fallback stack with as first entry +// a handler which send NotFound status (the default) func NewFallbackStack() *FallbackStack { return &FallbackStack{ handlers: context.Handlers{ diff --git a/core/router/fallback_stack_test.go b/core/router/fallback_stack_test.go new file mode 100644 index 00000000..1499cd96 --- /dev/null +++ b/core/router/fallback_stack_test.go @@ -0,0 +1,112 @@ +package router_test + +import ( + "testing" + + "github.com/kataras/iris" + "github.com/kataras/iris/context" + "github.com/kataras/iris/core/router" + "github.com/kataras/iris/httptest" +) + +func TestFallbackStackAdd(t *testing.T) { + l := make([]string, 0) + + stk := &router.FallbackStack{} + stk.Add(context.Handlers{ + func(context.Context) { + l = append(l, "POS1") + }, + }) + + stk.Add(context.Handlers{ + func(context.Context) { + l = append(l, "POS2") + }, + }) + + if stk.Size() != 2 { + t.Fatalf("Bad size (%d != 2)", stk.Size()) + } + + for _, h := range stk.List() { + h(nil) + } + + if (l[0] != "POS2") || (l[1] != "POS1") { + t.Fatal("Bad positions: ", l) + } +} + +func TestFallbackStackFork(t *testing.T) { + l := make([]string, 0) + + stk := &router.FallbackStack{} + + stk.Add(context.Handlers{ + func(context.Context) { + l = append(l, "POS1") + }, + }) + + stk.Add(context.Handlers{ + func(context.Context) { + l = append(l, "POS2") + }, + }) + + stk = stk.Fork() + + stk.Add(context.Handlers{ + func(context.Context) { + l = append(l, "POS3") + }, + }) + + stk.Add(context.Handlers{ + func(context.Context) { + l = append(l, "POS4") + }, + }) + + if stk.Size() != 4 { + t.Fatalf("Bad size (%d != 4)", stk.Size()) + } + + for _, h := range stk.List() { + h(nil) + } + + if (l[0] != "POS4") || (l[1] != "POS3") || (l[2] != "POS2") || (l[3] != "POS1") { + t.Fatal("Bad positions: ", l) + } +} + +func TestFallbackStackCall(t *testing.T) { + // build the api + app := iris.New() + + // setup an existing routes + app.Handle("GET", "/route", func(ctx context.Context) { + ctx.WriteString("ROUTED") + }) + + // setup fallback handler + app.Fallback(func(ctx context.Context) { + if ctx.Method() != "GET" { + ctx.Next() + + return + } + + ctx.WriteString("FALLBACK") + }) + + // run the tests + e := httptest.New(t, app, httptest.Debug(false)) + + e.Request("GET", "/route").Expect().Status(iris.StatusOK).Body().Equal("ROUTED") + e.Request("POST", "/route").Expect().Status(iris.StatusNotFound) + e.Request("POST", "/noroute").Expect().Status(iris.StatusNotFound) + e.Request("GET", "/noroute").Expect().Status(iris.StatusOK).Body().Equal("FALLBACK") +} diff --git a/core/router/handler.go b/core/router/handler.go index bd8aed9a..ce924493 100644 --- a/core/router/handler.go +++ b/core/router/handler.go @@ -22,6 +22,8 @@ type RequestHandler interface { HandleRequest(context.Context) // Build should builds the handler, it's being called on router's BuildRouter. Build(provider RoutesProvider) error + // RouteExists checks if a route exists + RouteExists(method, path string, ctx context.Context) bool } type tree struct { @@ -84,12 +86,13 @@ type RoutesProvider interface { // api builder GetRoutes() []*Route GetRoute(routeName string) *Route - FallBackStack() *FallbackStack + GetFallBackStack() *FallbackStack } func (h *routerHandler) Build(provider RoutesProvider) error { registeredRoutes := provider.GetRoutes() h.trees = h.trees[0:0] // reset, inneed when rebuilding. + h.fallbackStack = provider.GetFallBackStack() // sort, subdomains goes first. sort.Slice(registeredRoutes, func(i, j int) bool { @@ -245,5 +248,62 @@ func (h *routerHandler) HandleRequest(ctx context.Context) { } } - ctx.Do(h.fallbackStack.list()) + if h.fallbackStack == nil { + ctx.StatusCode(http.StatusNotFound) + } else { + ctx.Do(h.fallbackStack.List()) + } +} + +// RouteExists checks if a route exists +func (h *routerHandler) RouteExists(method, path string, ctx context.Context) bool { + for i := range h.trees { + t := h.trees[i] + if method != t.Method { + continue + } + + if h.hosts && t.Subdomain != "" { + requestHost := ctx.Host() + if netutil.IsLoopbackSubdomain(requestHost) { + // this fixes a bug when listening on + // 127.0.0.1:8080 for example + // and have a wildcard subdomain and a route registered to root domain. + continue // it's not a subdomain, it's something like 127.0.0.1 probably + } + // it's a dynamic wildcard subdomain, we have just to check if ctx.subdomain is not empty + if t.Subdomain == SubdomainWildcardIndicator { + // mydomain.com -> invalid + // localhost -> invalid + // sub.mydomain.com -> valid + // sub.localhost -> valid + serverHost := ctx.Application().ConfigurationReadOnly().GetVHost() + if serverHost == requestHost { + continue // it's not a subdomain, it's a full domain (with .com...) + } + + dotIdx := strings.IndexByte(requestHost, '.') + slashIdx := strings.IndexByte(requestHost, '/') + if dotIdx > 0 && (slashIdx == -1 || slashIdx > dotIdx) { + // if "." was found anywhere but not at the first path segment (host). + } else { + continue + } + // continue to that, any subdomain is valid. + } else if !strings.HasPrefix(requestHost, t.Subdomain) { // t.Subdomain contains the dot. + continue + } + } + + _, handlers := t.Nodes.Find(path, ctx.Params()) + if len(handlers) > 0 { + // found + return true + } + + // not found or method not allowed. + break + } + + return false } diff --git a/core/router/router.go b/core/router/router.go index 9b81d2ea..891813e1 100644 --- a/core/router/router.go +++ b/core/router/router.go @@ -147,6 +147,11 @@ func (router *Router) ServeHTTP(w http.ResponseWriter, r *http.Request) { router.mainHandler(w, r) } +// RouteExists checks if a route exists +func (router *Router) RouteExists(method, path string, ctx context.Context) bool { + return router.requestHandler.RouteExists(method, path, ctx) +} + type wrapper struct { router http.HandlerFunc // http.HandlerFunc to catch the CURRENT state of its .ServeHTTP on case of future change. wrapperFunc func(http.ResponseWriter, *http.Request, http.HandlerFunc) diff --git a/core/router/router_test.go b/core/router/router_test.go new file mode 100644 index 00000000..ba41a714 --- /dev/null +++ b/core/router/router_test.go @@ -0,0 +1,41 @@ +package router_test + +import ( + "testing" + + "github.com/kataras/iris" + "github.com/kataras/iris/context" + "github.com/kataras/iris/httptest" +) + +func TestRouteExists(t *testing.T) { + // build the api + app := iris.New() + emptyHandler := func(context.Context) {} + + // setup the tested routes + app.Handle("GET", "/route-exists", emptyHandler) + app.Handle("POST", "/route-with-param/{param}", emptyHandler) + + // check RouteExists + app.Handle("GET", "/route-test", func(ctx context.Context) { + if ctx.RouteExists("GET", "/route-not-exists") { + t.Error("Route with path should not exists") + } + + if ctx.RouteExists("POST", "/route-exists") { + t.Error("Route with method should not exists") + } + + if !ctx.RouteExists("GET", "/route-exists") { + t.Error("Route 1 should exists") + } + + if !ctx.RouteExists("POST", "/route-with-param/a-param") { + t.Error("Route 2 should exists") + } + }) + + // run the tests + httptest.New(t, app, httptest.Debug(false)).Request("GET", "/route-test").Expect().Status(iris.StatusOK) +}