From 227170fd33e9a422e74798fd61c76dc5080b4563 Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Sun, 16 Aug 2020 18:04:52 +0300 Subject: [PATCH] see https://github.com/kataras/iris/issues/1591#issuecomment-674473247 --- .../redirect/multi-instances/main.go | 2 +- cli.go | 2 +- core/router/router.go | 42 +++++++++ .../router_subdomain_redirect_wrapper.go | 48 ++++++++-- core/router/router_test.go | 87 +++++++++++++++++++ i18n/i18n.go | 8 +- i18n/loader.go | 15 ++++ iris.go | 12 +-- sessions/config.go | 2 +- 9 files changed, 199 insertions(+), 19 deletions(-) diff --git a/_examples/routing/subdomains/redirect/multi-instances/main.go b/_examples/routing/subdomains/redirect/multi-instances/main.go index 35f4a4ee..2c0bffa8 100644 --- a/_examples/routing/subdomains/redirect/multi-instances/main.go +++ b/_examples/routing/subdomains/redirect/multi-instances/main.go @@ -43,7 +43,7 @@ func createRoot(redirectTo string) *iris.Application { fullScheme = "https://" } - http.Redirect(w, r, fullScheme+redirectTo, iris.StatusMovedPermanently) + http.Redirect(w, r, fullScheme+redirectTo+r.URL.RequestURI(), iris.StatusMovedPermanently) }) return app diff --git a/cli.go b/cli.go index bed303b0..db4eb62a 100644 --- a/cli.go +++ b/cli.go @@ -115,6 +115,6 @@ func injectLiveReload(contextPool *context.Pool, router *router.Router) (bool, e contextPool.Release(ctx) } - router.WrapRouter(wrapper) + router.AddRouterWrapper(wrapper) return true, nil } diff --git a/core/router/router.go b/core/router/router.go index 2fd9071a..abdbe628 100644 --- a/core/router/router.go +++ b/core/router/router.go @@ -24,6 +24,14 @@ type Router struct { 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 + // wrappers to be built on BuildRouter state, + // first is executed first at this case. + // Case: + // - SubdomainRedirect on user call, registers a wrapper, on design state + // - i18n,if loaded and Subdomain or PathRedirect is true, registers a wrapper too, on build state + // the SubdomainRedirect should be the first(subdomainWrap(i18nWrap)) wrapper + // to be executed instead of last(i18nWrap(subdomainWrap)). + wrapperFuncs []WrapperFunc cPool *context.Pool // used on RefreshRouter routesProvider RoutesProvider @@ -216,6 +224,14 @@ func (router *Router) BuildRouter(cPool *context.Pool, requestHandler RequestHan } } + for i := len(router.wrapperFuncs) - 1; i >= 0; i-- { + w := router.wrapperFuncs[i] + if w == nil { + continue + } + router.WrapRouter(w) + } + if router.wrapperFunc != nil { // if wrapper used then attach that as the router service router.mainHandler = newWrapper(router.wrapperFunc, router.mainHandler).ServeHTTP } @@ -268,9 +284,35 @@ func (router *Router) Downgraded() bool { // // Before build. func (router *Router) WrapRouter(wrapperFunc WrapperFunc) { + // logger := context.DefaultLogger("router wrapper") + // file, line := context.HandlerFileLineRel(wrapperFunc) + // if router.wrapperFunc != nil { + // wrappedFile, wrappedLine := context.HandlerFileLineRel(router.wrapperFunc) + // logger.Infof("%s:%d wraps %s:%d", file, line, wrappedFile, wrappedLine) + // } else { + // logger.Infof("%s:%d wraps the main router", file, line) + // } router.wrapperFunc = makeWrapperFunc(router.wrapperFunc, wrapperFunc) } +// AddRouterWrapper adds a router wrapper. +// Unlike `WrapRouter` the first registered will be executed first +// so a wrapper wraps its next not the previous one. +// it defers the wrapping until the `BuildRouter`. +// Redirection wrappers should be added using this method +// e.g. SubdomainRedirect. +func (router *Router) AddRouterWrapper(wrapperFunc WrapperFunc) { + router.wrapperFuncs = append(router.wrapperFuncs, wrapperFunc) +} + +// PrependRouterWrapper like `AddRouterWrapper` but this wrapperFunc +// will always be executed before the previous `AddRouterWrapper`. +// Path form (no modification) wrappers should be added using this method +// e.g. ForceLowercaseRouting. +func (router *Router) PrependRouterWrapper(wrapperFunc WrapperFunc) { + router.wrapperFuncs = append([]WrapperFunc{wrapperFunc}, router.wrapperFuncs...) +} + // ServeHTTPC serves the raw context, useful if we have already a context, it by-pass the wrapper. func (router *Router) ServeHTTPC(ctx *context.Context) { router.requestHandler.HandleRequest(ctx) diff --git a/core/router/router_subdomain_redirect_wrapper.go b/core/router/router_subdomain_redirect_wrapper.go index a6ea31b8..dcbd5f44 100644 --- a/core/router/router_subdomain_redirect_wrapper.go +++ b/core/router/router_subdomain_redirect_wrapper.go @@ -1,8 +1,10 @@ package router import ( + "fmt" "net/http" "strings" + "text/template" "github.com/kataras/iris/v12/context" "github.com/kataras/iris/v12/core/netutil" @@ -42,7 +44,7 @@ func pathIsWildcard(partyRelPath string) bool { // // Usage(package-level): // sd := NewSubdomainRedirectWrapper(func() string { return "mydomain.com" }, ".", "www.") -// router.WrapRouter(sd) +// router.AddRouterWrapper(sd) // // Usage(high-level using `iris#Application.SubdomainRedirect`) // www := app.Subdomain("www") @@ -56,12 +58,12 @@ func pathIsWildcard(partyRelPath string) bool { // One or more subdomain redirect wrappers can be used to the same router instance. // // NewSubdomainRedirectWrapper may return nil if not allowed input arguments values were received -// but in that case, the `WrapRouter` will, simply, ignore that wrapper. +// but in that case, the `AddRouterWrapper` will, simply, ignore that wrapper. // // Example: https://github.com/kataras/iris/tree/master/_examples/routing/subdomains/redirect func NewSubdomainRedirectWrapper(rootDomainGetter func() string, from, to string) WrapperFunc { // we can return nil, - // because if wrapper is nil then it's not be used on the `router#WrapRouter`. + // because if wrapper is nil then it's not be used on the `router#AddRouterWrapper`. if from == to { // cannot redirect to the same location, cycle. return nil @@ -109,7 +111,6 @@ func (s *subdomainRedirectWrapper) Wrapper(w http.ResponseWriter, r *http.Reques // because older browsers may not be able to recognise that status code (the RFC 7538, is not so old) // although note that move is not the same thing as redirect: move reminds a specific address or location moved while // redirect is a new location. - host := context.GetHost(r) root := s.root() if loopback := netutil.GetLoopbackSubdomain(root); loopback != "" { @@ -117,7 +118,6 @@ func (s *subdomainRedirectWrapper) Wrapper(w http.ResponseWriter, r *http.Reques } hasSubdomain := host != root - if !hasSubdomain && !s.isFromRoot { // if the current endpoint is not a subdomain // and the redirect is not configured to be used from root domain to a subdomain. @@ -142,14 +142,25 @@ func (s *subdomainRedirectWrapper) Wrapper(w http.ResponseWriter, r *http.Reques resturi := r.URL.RequestURI() if s.isToRoot { // from a specific subdomain or any subdomain to the root domain. - http.Redirect(w, r, getFullScheme(r)+root+resturi, http.StatusMovedPermanently) + redirectAbsolute(w, r, getFullScheme(r)+root+resturi, http.StatusMovedPermanently) return } // from a specific subdomain or any subdomain to a specific subdomain. - http.Redirect(w, r, getFullScheme(r)+s.to+root+resturi, http.StatusMovedPermanently) + redirectAbsolute(w, r, getFullScheme(r)+s.to+root+resturi, http.StatusMovedPermanently) return } + if s.isFromRoot && !s.isFromAny { + // Then we must not continue, + // the subdomain didn't match the "to" but the from + // was the application root itself, which is not a wildcard + // so it shouldn't accept any subdomain, we must fire 404 here. + // Something like: + // http://registered_host_but_not_in_app.your.mydomain.com + http.NotFound(w, r) + return + + } // the from subdomain is not matched and it's not from root. router(w, r) return @@ -159,9 +170,30 @@ func (s *subdomainRedirectWrapper) Wrapper(w http.ResponseWriter, r *http.Reques resturi := r.URL.RequestURI() // we are not inside a subdomain, so we are in the root domain // and the redirect is configured to be used from root domain to a subdomain. - http.Redirect(w, r, getFullScheme(r)+s.to+root+resturi, http.StatusMovedPermanently) + redirectAbsolute(w, r, getFullScheme(r)+s.to+root+resturi, http.StatusMovedPermanently) return } router(w, r) } + +func redirectAbsolute(w http.ResponseWriter, r *http.Request, url string, code int) { + h := w.Header() + + // RFC 7231 notes that a short HTML body is usually included in + // the response because older user agents may not understand 301/307. + // Do it only if the request didn't already have a Content-Type header. + _, hadCT := h[context.ContentTypeHeaderKey] + + h.Set("Location", url) + if !hadCT && (r.Method == http.MethodGet || r.Method == http.MethodHead) { + h.Set(context.ContentTypeHeaderKey, "text/html; charset=utf-8") + } + w.WriteHeader(code) + + // Shouldn't send the body for POST or HEAD; that leaves GET. + if !hadCT && r.Method == "GET" { + body := "" + http.StatusText(code) + ".\n" + fmt.Fprintln(w, body) + } +} diff --git a/core/router/router_test.go b/core/router/router_test.go index 535cb7bd..17f109de 100644 --- a/core/router/router_test.go +++ b/core/router/router_test.go @@ -1,12 +1,14 @@ package router_test import ( + "io" "net/http" "strings" "testing" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/context" + "github.com/kataras/iris/v12/core/router" "github.com/kataras/iris/v12/httptest" ) @@ -72,3 +74,88 @@ func TestLowercaseRouting(t *testing.T) { e.GET(strings.ToUpper(tt)).Expect().Status(httptest.StatusOK).Body().Equal(s) } } + +func TestRouterWrapperOrder(t *testing.T) { + // last is wrapping the previous. + + // first is executed last. + userWrappers := []router.WrapperFunc{ + func(w http.ResponseWriter, r *http.Request, main http.HandlerFunc) { + io.WriteString(w, "6") + main(w, r) + }, + func(w http.ResponseWriter, r *http.Request, main http.HandlerFunc) { + io.WriteString(w, "5") + main(w, r) + }, + } + // should be executed before userWrappers. + redirectionWrappers := []router.WrapperFunc{ + func(w http.ResponseWriter, r *http.Request, main http.HandlerFunc) { + io.WriteString(w, "3") + main(w, r) + }, + func(w http.ResponseWriter, r *http.Request, main http.HandlerFunc) { + io.WriteString(w, "4") + main(w, r) + }, + } + // should be executed before redirectionWrappers. + afterRedirectionWrappers := []router.WrapperFunc{ + func(w http.ResponseWriter, r *http.Request, main http.HandlerFunc) { + io.WriteString(w, "2") + main(w, r) + }, + func(w http.ResponseWriter, r *http.Request, main http.HandlerFunc) { + io.WriteString(w, "1") + main(w, r) + }, + } + + testOrder1 := iris.New() + for _, w := range userWrappers { + testOrder1.WrapRouter(w) + // this always wraps the previous one, but it's not accessible after Build state, + // the below are simulating the SubdomainRedirect and ForceLowercaseRouting. + } + for _, w := range redirectionWrappers { + testOrder1.AddRouterWrapper(w) + } + for _, w := range afterRedirectionWrappers { + testOrder1.PrependRouterWrapper(w) + } + + testOrder2 := iris.New() + for _, w := range redirectionWrappers { + testOrder2.AddRouterWrapper(w) + } + for _, w := range userWrappers { + testOrder2.WrapRouter(w) + } + for _, w := range afterRedirectionWrappers { + testOrder2.PrependRouterWrapper(w) + } + + testOrder3 := iris.New() + for _, w := range redirectionWrappers { + testOrder3.AddRouterWrapper(w) + } + for _, w := range afterRedirectionWrappers { + testOrder3.PrependRouterWrapper(w) + } + for _, w := range userWrappers { + testOrder3.WrapRouter(w) + } + + appTests := []*iris.Application{ + testOrder1, testOrder2, testOrder3, + } + + expectedOrderStr := "123456" + for _, app := range appTests { + app.Get("/", func(ctx iris.Context) {}) // to not append the not found one. + + e := httptest.New(t, app) + e.GET("/").Expect().Status(iris.StatusOK).Body().Equal(expectedOrderStr) + } +} diff --git a/i18n/i18n.go b/i18n/i18n.go index 667d5c2c..945e822d 100644 --- a/i18n/i18n.go +++ b/i18n/i18n.go @@ -406,7 +406,7 @@ func (i *I18n) GetMessage(ctx *context.Context, format string, args ...interface } // Wrapper returns a new router wrapper. -// The result function can be passed on `Application.WrapRouter`. +// The result function can be passed on `Application.WrapRouter/AddRouterWrapper`. // It compares the path prefix for translated language and // local redirects the requested path with the selected (from the path) language to the router. // @@ -417,7 +417,10 @@ func (i *I18n) Wrapper() router.WrapperFunc { } return func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) { found := false - path := r.URL.Path[1:] + path := r.URL.Path + if len(path) > 0 && path[0] == '/' { + path = path[1:] + } if idx := strings.IndexByte(path, '/'); idx > 0 { path = path[:idx] @@ -451,7 +454,6 @@ func (i *I18n) Wrapper() router.WrapperFunc { } } } - } next(w, r) diff --git a/i18n/loader.go b/i18n/loader.go index 5ef36934..77aee50f 100644 --- a/i18n/loader.go +++ b/i18n/loader.go @@ -172,6 +172,21 @@ func (l MemoryLocalizer) GetLocale(index int) context.Locale { // panic(fmt.Sprintf("locale of index [%d] not found", index)) // } // return loc + /* Note(@kataras): the following is allowed as a language index can be higher + than the length of the locale files. + if index >= len(l) || index < 0 { + // 1. language exists in the caller but was not found in files. + // 2. language exists in both files and caller but the actual + // languages are two, while the registered are 4 (when missing files), + // that happens when Strict option is false. + // force to the default language but what is the default language if the language index is greater than this length? + // That's why it's allowed. + index = 0 + }*/ + + if index < 0 { + index = 0 + } return l[index] } diff --git a/iris.go b/iris.go index a73e765e..e16a3c2a 100644 --- a/iris.go +++ b/iris.go @@ -162,9 +162,7 @@ func (app *Application) WWW() router.Party { // Example: https://github.com/kataras/iris/tree/master/_examples/routing/subdomains/redirect func (app *Application) SubdomainRedirect(from, to router.Party) router.Party { sd := router.NewSubdomainRedirectWrapper(app.ConfigurationReadOnly().GetVHost, from.GetRelPath(), to.GetRelPath()) - // TODO: add a debug message here or wait for a response from the issuer - // so we can force these to run on build state (last registered, first executed). - app.Router.WrapRouter(sd) + app.Router.AddRouterWrapper(sd) return to } @@ -489,6 +487,7 @@ func (app *Application) Build() error { if app.builded { return nil } + // start := time.Now() app.builded = true // even if fails. @@ -532,7 +531,7 @@ func (app *Application) Build() error { if app.I18n.Loaded() { // {{ tr "lang" "key" arg1 arg2 }} app.view.AddFunc("tr", app.I18n.Tr) - app.Router.WrapRouter(app.I18n.Wrapper()) + app.Router.AddRouterWrapper(app.I18n.Wrapper()) } if n := app.view.Len(); n > 0 { @@ -560,7 +559,10 @@ func (app *Application) Build() error { } if app.config.ForceLowercaseRouting { - app.Router.WrapRouter(func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) { + // This should always be executed first. + app.Router.PrependRouterWrapper(func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) { + r.Host = strings.ToLower(r.Host) + r.URL.Host = strings.ToLower(r.URL.Host) r.URL.Path = strings.ToLower(r.URL.Path) next(w, r) }) diff --git a/sessions/config.go b/sessions/config.go index ce7026d8..a15fe6b8 100644 --- a/sessions/config.go +++ b/sessions/config.go @@ -18,7 +18,7 @@ type ( // Config is the configuration for sessions. Please read it before using sessions. Config struct { // Logger instance for sessions usage, e.g. { Logger: app.Logger() }. - // Defauls to a child of "sessions" of the latest Iris Application's main Logger. + // Defaults to a child of "sessions" of the latest Iris Application's main Logger. Logger *golog.Logger // Cookie string, the session's client cookie name, for example: "mysessionid" //