package router import ( "fmt" "net/http" "sort" "strings" "time" "github.com/kataras/iris/v12/context" "github.com/kataras/iris/v12/core/errgroup" "github.com/kataras/iris/v12/core/netutil" macroHandler "github.com/kataras/iris/v12/macro/handler" "github.com/kataras/golog" "github.com/kataras/pio" ) type ( // RequestHandler the middle man between acquiring a context and releasing it. // By-default is the router algorithm. RequestHandler interface { // Note: A different interface in order to hide the rest of the implementation. // We only need the `FireErrorCode` to be accessible through the Iris application (see `iris.go#Build`) HTTPErrorHandler // HandleRequest should handle the request based on the Context. HandleRequest(ctx *context.Context) // Build should builds the handler, it's being called on router's BuildRouter. Build(provider RoutesProvider) error // RouteExists reports whether a particular route exists. RouteExists(ctx *context.Context, method, path string) bool } // HTTPErrorHandler should contain a method `FireErrorCode` which // handles http unsuccessful status codes. HTTPErrorHandler interface { // FireErrorCode should send an error response to the client based // on the given context's response status code. FireErrorCode(ctx *context.Context) } ) type routerHandler struct { // Config. disablePathCorrection bool disablePathCorrectionRedirection bool fireMethodNotAllowed bool enablePathIntelligence bool forceLowercaseRouting bool // logger *golog.Logger trees []*trie errorTrees []*trie hosts bool // true if at least one route contains a Subdomain. errorHosts bool // true if error handlers are registered to at least one Subdomain. errorDefaultHandlers context.Handlers // the main handler(s) for default error code handlers, when not registered directly by the end-developer. } var _ RequestHandler = (*routerHandler)(nil) var _ HTTPErrorHandler = (*routerHandler)(nil) // NewDefaultHandler returns the handler which is responsible // to map the request with a route (aka mux implementation). func NewDefaultHandler(config context.ConfigurationReadOnly, logger *golog.Logger) RequestHandler { return &routerHandler{ disablePathCorrection: config.GetDisablePathCorrection(), disablePathCorrectionRedirection: config.GetDisablePathCorrectionRedirection(), fireMethodNotAllowed: config.GetFireMethodNotAllowed(), enablePathIntelligence: config.GetEnablePathIntelligence(), forceLowercaseRouting: config.GetForceLowercaseRouting(), logger: logger, } } func (h *routerHandler) getTree(statusCode int, method, subdomain string) *trie { if statusCode > 0 { for i := range h.errorTrees { t := h.errorTrees[i] if t.statusCode == statusCode && t.subdomain == subdomain { return t } } return nil } for i := range h.trees { t := h.trees[i] if t.method == method && t.subdomain == subdomain { return t } } return nil } // AddRoute registers a route. See `Router.AddRouteUnsafe`. func (h *routerHandler) AddRoute(r *Route) error { var ( method = r.Method statusCode = r.StatusCode subdomain = r.Subdomain path = r.Path handlers = r.Handlers ) t := h.getTree(statusCode, method, subdomain) if t == nil { n := newTrieNode() // first time we register a route to this method with this subdomain t = &trie{statusCode: statusCode, method: method, subdomain: subdomain, root: n} if statusCode > 0 { h.errorTrees = append(h.errorTrees, t) } else { h.trees = append(h.trees, t) } } t.insert(path, r.ReadOnly, handlers) return nil } // RoutesProvider should be implemented by // iteral which contains the registered routes. 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 // GetDefaultErrorMiddleware should return // the default error handler middleares. GetDefaultErrorMiddleware() context.Handlers } func defaultErrorHandler(ctx *context.Context) { if ok, err := ctx.GetErrPublic(); ok { // If an error is stored and it's not a private one // write it to the response body. ctx.WriteString(err.Error()) return } // Otherwise, write the code's text instead. ctx.WriteString(context.StatusText(ctx.GetStatusCode())) } func (h *routerHandler) Build(provider RoutesProvider) error { h.trees = h.trees[0:0] // reset, inneed when rebuilding. h.errorTrees = h.errorTrees[0:0] // set the default error code handler, will be fired on error codes // that are not handled by a specific handler (On(Any)ErrorCode). h.errorDefaultHandlers = append(provider.GetDefaultErrorMiddleware(), defaultErrorHandler) rp := errgroup.New("Routes Builder") registeredRoutes := provider.GetRoutes() // before sort. for _, r := range registeredRoutes { if r.topLink != nil { bindMultiParamTypesHandler(r) } } // sort, subdomains go first. sort.Slice(registeredRoutes, func(i, j int) bool { first, second := registeredRoutes[i], registeredRoutes[j] lsub1 := len(first.Subdomain) lsub2 := len(second.Subdomain) firstSlashLen := strings.Count(first.Path, "/") secondSlashLen := strings.Count(second.Path, "/") if lsub1 == lsub2 && first.Method == second.Method { if secondSlashLen < firstSlashLen { // fixes order when wildcard root is registered before other wildcard paths return true } if secondSlashLen == firstSlashLen { // fixes order when static path with the same prefix with a wildcard path // is registered after the wildcard path, although this is managed // by the low-level node but it couldn't work if we registered a root level wildcard, this fixes it. if len(first.tmpl.Params) == 0 { return false } if len(second.tmpl.Params) == 0 { return true } // No don't fix the order by framework's suggestion, // let it as it is today; {string} and {path} should be registered before {id} {uint} and e.t.c. // see `bindMultiParamTypesHandler` for the reason. Order of registration matters. } } // the rest are handled inside the node return lsub1 > lsub2 }) noLogCount := 0 for _, r := range registeredRoutes { if r.NoLog { noLogCount++ } if h.forceLowercaseRouting { // only in that state, keep everything else as end-developer registered. r.Path = strings.ToLower(r.Path) } if r.Subdomain != "" { if r.StatusCode > 0 { h.errorHosts = true } else { h.hosts = true } } if r.topLink == nil { // build the r.Handlers based on begin and done handlers, if any. r.BuildHandlers() // the only "bad" with this is if the user made an error // on route, it will be stacked shown in this build state // and no in the lines of the user's action, they should read // the docs better. Or TODO: add a link here in order to help new users. if err := h.AddRoute(r); err != nil { // node errors: rp.Addf("%s: %w", r.String(), err) continue } } } // TODO: move this and make it easier to read when all cases are, visually, tested. if logger := h.logger; logger != nil && logger.Level == golog.DebugLevel && noLogCount < len(registeredRoutes) { // group routes by method and print them without the [DBUG] and time info, // the route logs are colorful. // Note: don't use map, we need to keep registered order, use // different slices for each method. collect := func(method string) (methodRoutes []*Route) { for _, r := range registeredRoutes { if r.NoLog { continue } if r.Method == method { methodRoutes = append(methodRoutes, r) } } return } type MethodRoutes struct { method string routes []*Route } allMethods := append(AllMethods, []string{MethodNone, ""}...) methodRoutes := make([]MethodRoutes, 0, len(allMethods)) for _, method := range allMethods { routes := collect(method) if len(routes) > 0 { methodRoutes = append(methodRoutes, MethodRoutes{method, routes}) } } if n := len(methodRoutes); n > 0 { tr := "routes" if len(registeredRoutes) == 1 { tr = tr[0 : len(tr)-1] } bckpNewLine := logger.NewLine logger.NewLine = false debugLevel := golog.Levels[golog.DebugLevel] // Replace that in order to not transfer it to the log handler (e.g. json) // logger.Debugf("API: %d registered %s (", len(registeredRoutes), tr) // with: pio.WriteRich(logger.Printer, debugLevel.Title, debugLevel.ColorCode, debugLevel.Style...) fmt.Fprintf(logger.Printer, " %s %sAPI: %d registered %s (", time.Now().Format(logger.TimeFormat), logger.Prefix, len(registeredRoutes)-noLogCount, tr) // logger.NewLine = bckpNewLine for i, m := range methodRoutes { // @method: @count if i > 0 { if i == n-1 { fmt.Fprint(logger.Printer, " and ") } else { fmt.Fprint(logger.Printer, ", ") } } if m.method == "" { m.method = "ERROR" } fmt.Fprintf(logger.Printer, "%d ", len(m.routes)) pio.WriteRich(logger.Printer, m.method, TraceTitleColorCode(m.method)) } fmt.Fprint(logger.Printer, ")\n") } for i, m := range methodRoutes { for _, r := range m.routes { r.Trace(logger.Printer, -1) } if i != len(allMethods)-1 { logger.Printer.Write(pio.NewLine) } } } return errgroup.Check(rp) } func bindMultiParamTypesHandler(r *Route) { // like overlap feature but specifically for path parameters. r.BuildHandlers() h := r.Handlers[1:] // remove the macro evaluator handler as we manually check below. f := macroHandler.MakeFilter(r.tmpl) if f == nil { return // should never happen, previous checks made to set the top link. } currentStatusCode := r.StatusCode if currentStatusCode == 0 { currentStatusCode = http.StatusOK } decisionHandler := func(ctx *context.Context) { // println("core/router/handler.go: decision handler; " + ctx.Path() + " route.Name: " + r.Name + " vs context's " + ctx.GetCurrentRoute().Name()) currentRoute := ctx.GetCurrentRoute() // Different path parameters types in the same path, fallback should registered first e.g. {path} {string}, // because the handler on this case is executing from last to top. if f(ctx) { // println("core/router/handler.go: filter for : " + r.Name + " passed") ctx.SetCurrentRoute(r.ReadOnly) // Note: error handlers will be the same, routes came from the same party, // no need to update them. ctx.HandlerIndex(0) ctx.Do(h) return } ctx.SetCurrentRoute(currentRoute) ctx.StatusCode(currentStatusCode) ctx.Next() } r.topLink.builtinBeginHandlers = append(context.Handlers{decisionHandler}, r.topLink.builtinBeginHandlers...) } func canHandleSubdomain(ctx *context.Context, subdomain string) bool { if subdomain == "" { return true } 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. return false // 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 subdomain == SubdomainWildcardIndicator { // mydomain.com -> invalid // localhost -> invalid // sub.mydomain.com -> valid // sub.localhost -> valid serverHost := ctx.Application().ConfigurationReadOnly().GetVHost() if serverHost == requestHost { return false // 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 { return false } // continue to that, any subdomain is valid. } else if !strings.HasPrefix(requestHost, subdomain) { // subdomain contains the dot, e.g. "admin." return false } return true } func (h *routerHandler) HandleRequest(ctx *context.Context) { method := ctx.Method() path := ctx.Path() if !h.disablePathCorrection { if len(path) > 1 && strings.HasSuffix(path, "/") { // Remove trailing slash and client-permanent rule for redirection, // if confgiuration allows that and path has an extra slash. // update the new path and redirect. u := ctx.Request().URL // use Trim to ensure there is no open redirect due to two leading slashes path = "/" + strings.Trim(path, "/") u.Path = path if !h.disablePathCorrectionRedirection { // do redirect, else continue with the modified path without the last "/". url := u.String() // Fixes https://github.com/kataras/iris/issues/921 // This is caused for security reasons, imagine a payment shop, // you can't just permantly redirect a POST request, so just 307 (RFC 7231, 6.4.7). if method == http.MethodPost || method == http.MethodPut { ctx.Redirect(url, http.StatusTemporaryRedirect) return } ctx.Redirect(url, http.StatusMovedPermanently) return } } } for i := range h.trees { t := h.trees[i] if method != t.method { continue } if h.hosts && !canHandleSubdomain(ctx, t.subdomain) { continue } n := t.search(path, ctx.Params()) if n != nil { ctx.SetCurrentRoute(n.Route) ctx.Do(n.Handlers) // found return } // not found or method not allowed. break } if h.fireMethodNotAllowed { for i := range h.trees { t := h.trees[i] // if `Configuration#FireMethodNotAllowed` is kept as defaulted(false) then this function will not // run, therefore performance kept as before. if h.subdomainAndPathAndMethodExists(ctx, t, "", path) { // RCF rfc2616 https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html // The response MUST include an Allow header containing a list of valid methods for the requested resource. ctx.Header("Allow", t.method) ctx.StatusCode(http.StatusMethodNotAllowed) return } } } if h.enablePathIntelligence && method == http.MethodGet { closestPaths := ctx.FindClosest(1) if len(closestPaths) > 0 { u := ctx.Request().URL u.Path = closestPaths[0] ctx.Redirect(u.String(), http.StatusMovedPermanently) return } } ctx.StatusCode(http.StatusNotFound) } func statusCodeSuccessful(statusCode int) bool { return !context.StatusCodeNotSuccessful(statusCode) } // FireErrorCode handles the response's error response. // If `Configuration.ResetOnFireErrorCode()` is true // and the response writer was a recorder one // then it will try to reset the headers and the body before calling the // registered (or default) error handler for that error code set by // `ctx.StatusCode` method. func (h *routerHandler) FireErrorCode(ctx *context.Context) { // On common response writer, always check // if we can't reset the body and the body has been filled // which means that the status code already sent, // then do not fire this custom error code, // rel: context/context.go#EndRequest. // // Note that, this is set to 0 on recorder because it holds the response before sent, // so we check their len(Body()) instead, look below. if ctx.ResponseWriter().Written() > 0 { return } statusCode := ctx.GetStatusCode() // the response's cached one. if ctx.Application().ConfigurationReadOnly().GetResetOnFireErrorCode() /* could be an argument too but we must not break the method */ { // if we can reset the body, probably manual call of `Application.FireErrorCode`. if w, ok := ctx.IsRecording(); ok { if statusCodeSuccessful(w.StatusCode()) { // if not an error status code w.WriteHeader(statusCode) // then set it manually here, otherwise it should be set via ctx.StatusCode(...) } // reset if previous content and it's recorder, keep the status code. w.ClearHeaders() w.ResetBody() if cw, ok := w.ResponseWriter.(*context.CompressResponseWriter); ok { // recorder wraps a compress writer. cw.Disabled = true } } else if w, ok := ctx.ResponseWriter().(*context.CompressResponseWriter); ok { // reset and disable the gzip in order to be an expected form of http error result w.Disabled = true } } else { // check if a body already set (the error response is handled by the handler itself, // see `Context.EndRequest`) if w, ok := ctx.IsRecording(); ok { if len(w.Body()) > 0 { return } } } for i := range h.errorTrees { t := h.errorTrees[i] if statusCode != t.statusCode { continue } if h.errorHosts && !canHandleSubdomain(ctx, t.subdomain) { continue } n := t.search(ctx.Path(), ctx.Params()) if n == nil { // try to take the root's one. n = t.root.getChild(pathSep) } if n != nil { // Note: handlers can contain macro filters here, // they are registered as error handlers, see macro/handler.go#42. // fmt.Println("Error Handlers") // for _, h := range n.Handlers { // f, l := context.HandlerFileLine(h) // fmt.Printf("%s: %s:%d\n", ctx.Path(), f, l) // } // fire this http status code's error handlers chain. // ctx.StopExecution() // not uncomment this, is here to remember why to. // note for me: I don't stopping the execution of the other handlers // because may the user want to add a fallback error code // i.e // users := app.Party("/users") // users.Done(func(ctx *context.Context){ if ctx.StatusCode() == 400 { /* custom error code for /users */ }}) // use .HandlerIndex // that sets the current handler index to zero // in order to: // ignore previous runs that may changed the handler index, // via ctx.Next or ctx.StopExecution, if any. // // use .Do // that overrides the existing handlers and sets and runs these error handlers. // in order to: // ignore the route's after-handlers, if any. ctx.SetCurrentRoute(n.Route) // Should work with: // Manual call of ctx.Application().FireErrorCode(ctx) (handlers length > 0) // And on `ctx.SetStatusCode`: Context -> EndRequest -> FireErrorCode (handlers length > 0) // And on router: HandleRequest -> SetStatusCode -> Context -> // EndRequest -> FireErrorCode (handlers' length is always 0) ctx.HandlerIndex(0) ctx.Do(n.Handlers) return } break } // not error handler found, // see if failed with a stored error, and if so // then render it, otherwise write a default message. ctx.Do(h.errorDefaultHandlers) } func (h *routerHandler) subdomainAndPathAndMethodExists(ctx *context.Context, t *trie, method, path string) bool { if method != "" && method != t.method { return false } 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. return false // 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 { return false // 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 { return false } // continue to that, any subdomain is valid. } else if !strings.HasPrefix(requestHost, t.subdomain) { // t.subdomain contains the dot. return false } } n := t.search(path, ctx.Params()) return n != nil } // RouteExists reports whether a particular route exists // It will search from the current subdomain of context's host, if not inside the root domain. func (h *routerHandler) RouteExists(ctx *context.Context, method, path string) bool { for i := range h.trees { t := h.trees[i] if h.subdomainAndPathAndMethodExists(ctx, t, method, path) { return true } } return false }