Gerasimos (Makis) Maropoulos 2020-08-16 18:04:52 +03:00
parent 889b7942d3
commit 227170fd33
No known key found for this signature in database
GPG Key ID: 5DBE766BD26A54E7
9 changed files with 199 additions and 19 deletions

View File

@ -43,7 +43,7 @@ func createRoot(redirectTo string) *iris.Application {
fullScheme = "https://" fullScheme = "https://"
} }
http.Redirect(w, r, fullScheme+redirectTo, iris.StatusMovedPermanently) http.Redirect(w, r, fullScheme+redirectTo+r.URL.RequestURI(), iris.StatusMovedPermanently)
}) })
return app return app

2
cli.go
View File

@ -115,6 +115,6 @@ func injectLiveReload(contextPool *context.Pool, router *router.Router) (bool, e
contextPool.Release(ctx) contextPool.Release(ctx)
} }
router.WrapRouter(wrapper) router.AddRouterWrapper(wrapper)
return true, nil return true, nil
} }

View File

@ -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. requestHandler RequestHandler // build-accessible, can be changed to define a custom router or proxy, used on RefreshRouter too.
mainHandler http.HandlerFunc // init-accessible mainHandler http.HandlerFunc // init-accessible
wrapperFunc WrapperFunc 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 cPool *context.Pool // used on RefreshRouter
routesProvider RoutesProvider 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 if router.wrapperFunc != nil { // if wrapper used then attach that as the router service
router.mainHandler = newWrapper(router.wrapperFunc, router.mainHandler).ServeHTTP router.mainHandler = newWrapper(router.wrapperFunc, router.mainHandler).ServeHTTP
} }
@ -268,9 +284,35 @@ func (router *Router) Downgraded() bool {
// //
// Before build. // Before build.
func (router *Router) WrapRouter(wrapperFunc WrapperFunc) { 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) 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. // ServeHTTPC serves the raw context, useful if we have already a context, it by-pass the wrapper.
func (router *Router) ServeHTTPC(ctx *context.Context) { func (router *Router) ServeHTTPC(ctx *context.Context) {
router.requestHandler.HandleRequest(ctx) router.requestHandler.HandleRequest(ctx)

View File

@ -1,8 +1,10 @@
package router package router
import ( import (
"fmt"
"net/http" "net/http"
"strings" "strings"
"text/template"
"github.com/kataras/iris/v12/context" "github.com/kataras/iris/v12/context"
"github.com/kataras/iris/v12/core/netutil" "github.com/kataras/iris/v12/core/netutil"
@ -42,7 +44,7 @@ func pathIsWildcard(partyRelPath string) bool {
// //
// Usage(package-level): // Usage(package-level):
// sd := NewSubdomainRedirectWrapper(func() string { return "mydomain.com" }, ".", "www.") // sd := NewSubdomainRedirectWrapper(func() string { return "mydomain.com" }, ".", "www.")
// router.WrapRouter(sd) // router.AddRouterWrapper(sd)
// //
// Usage(high-level using `iris#Application.SubdomainRedirect`) // Usage(high-level using `iris#Application.SubdomainRedirect`)
// www := app.Subdomain("www") // 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. // 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 // 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 // Example: https://github.com/kataras/iris/tree/master/_examples/routing/subdomains/redirect
func NewSubdomainRedirectWrapper(rootDomainGetter func() string, from, to string) WrapperFunc { func NewSubdomainRedirectWrapper(rootDomainGetter func() string, from, to string) WrapperFunc {
// we can return nil, // 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 { if from == to {
// cannot redirect to the same location, cycle. // cannot redirect to the same location, cycle.
return nil 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) // 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 // 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. // redirect is a new location.
host := context.GetHost(r) host := context.GetHost(r)
root := s.root() root := s.root()
if loopback := netutil.GetLoopbackSubdomain(root); loopback != "" { if loopback := netutil.GetLoopbackSubdomain(root); loopback != "" {
@ -117,7 +118,6 @@ func (s *subdomainRedirectWrapper) Wrapper(w http.ResponseWriter, r *http.Reques
} }
hasSubdomain := host != root hasSubdomain := host != root
if !hasSubdomain && !s.isFromRoot { if !hasSubdomain && !s.isFromRoot {
// if the current endpoint is not a subdomain // if the current endpoint is not a subdomain
// and the redirect is not configured to be used from root domain to 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() resturi := r.URL.RequestURI()
if s.isToRoot { if s.isToRoot {
// from a specific subdomain or any subdomain to the root domain. // 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 return
} }
// from a specific subdomain or any subdomain to a specific subdomain. // 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 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. // the from subdomain is not matched and it's not from root.
router(w, r) router(w, r)
return return
@ -159,9 +170,30 @@ func (s *subdomainRedirectWrapper) Wrapper(w http.ResponseWriter, r *http.Reques
resturi := r.URL.RequestURI() resturi := r.URL.RequestURI()
// we are not inside a subdomain, so we are in the root domain // 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. // 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 return
} }
router(w, r) 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 := "<a href=\"" + template.HTMLEscapeString(url) + "\">" + http.StatusText(code) + "</a>.\n"
fmt.Fprintln(w, body)
}
}

View File

@ -1,12 +1,14 @@
package router_test package router_test
import ( import (
"io"
"net/http" "net/http"
"strings" "strings"
"testing" "testing"
"github.com/kataras/iris/v12" "github.com/kataras/iris/v12"
"github.com/kataras/iris/v12/context" "github.com/kataras/iris/v12/context"
"github.com/kataras/iris/v12/core/router"
"github.com/kataras/iris/v12/httptest" "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) 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)
}
}

View File

@ -406,7 +406,7 @@ func (i *I18n) GetMessage(ctx *context.Context, format string, args ...interface
} }
// Wrapper returns a new router wrapper. // 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 // It compares the path prefix for translated language and
// local redirects the requested path with the selected (from the path) language to the router. // 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) { return func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
found := false 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 { if idx := strings.IndexByte(path, '/'); idx > 0 {
path = path[:idx] path = path[:idx]
@ -451,7 +454,6 @@ func (i *I18n) Wrapper() router.WrapperFunc {
} }
} }
} }
} }
next(w, r) next(w, r)

View File

@ -172,6 +172,21 @@ func (l MemoryLocalizer) GetLocale(index int) context.Locale {
// panic(fmt.Sprintf("locale of index [%d] not found", index)) // panic(fmt.Sprintf("locale of index [%d] not found", index))
// } // }
// return loc // 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] return l[index]
} }

12
iris.go
View File

@ -162,9 +162,7 @@ func (app *Application) WWW() router.Party {
// Example: https://github.com/kataras/iris/tree/master/_examples/routing/subdomains/redirect // Example: https://github.com/kataras/iris/tree/master/_examples/routing/subdomains/redirect
func (app *Application) SubdomainRedirect(from, to router.Party) router.Party { func (app *Application) SubdomainRedirect(from, to router.Party) router.Party {
sd := router.NewSubdomainRedirectWrapper(app.ConfigurationReadOnly().GetVHost, from.GetRelPath(), to.GetRelPath()) sd := router.NewSubdomainRedirectWrapper(app.ConfigurationReadOnly().GetVHost, from.GetRelPath(), to.GetRelPath())
// TODO: add a debug message here or wait for a response from the issuer app.Router.AddRouterWrapper(sd)
// so we can force these to run on build state (last registered, first executed).
app.Router.WrapRouter(sd)
return to return to
} }
@ -489,6 +487,7 @@ func (app *Application) Build() error {
if app.builded { if app.builded {
return nil return nil
} }
// start := time.Now() // start := time.Now()
app.builded = true // even if fails. app.builded = true // even if fails.
@ -532,7 +531,7 @@ func (app *Application) Build() error {
if app.I18n.Loaded() { if app.I18n.Loaded() {
// {{ tr "lang" "key" arg1 arg2 }} // {{ tr "lang" "key" arg1 arg2 }}
app.view.AddFunc("tr", app.I18n.Tr) 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 { if n := app.view.Len(); n > 0 {
@ -560,7 +559,10 @@ func (app *Application) Build() error {
} }
if app.config.ForceLowercaseRouting { 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) r.URL.Path = strings.ToLower(r.URL.Path)
next(w, r) next(w, r)
}) })

View File

@ -18,7 +18,7 @@ type (
// Config is the configuration for sessions. Please read it before using sessions. // Config is the configuration for sessions. Please read it before using sessions.
Config struct { Config struct {
// Logger instance for sessions usage, e.g. { Logger: app.Logger() }. // 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 Logger *golog.Logger
// Cookie string, the session's client cookie name, for example: "mysessionid" // Cookie string, the session's client cookie name, for example: "mysessionid"
// //