From 1780d97d4458d2df73fd655ba61a70e1c07bd595 Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Wed, 26 Aug 2020 00:07:07 +0300 Subject: [PATCH] update the subdomain redirect example using the rewrite middleware --- _examples/routing/subdomains/redirect/main.go | 82 ++++++++----------- core/router/api_builder.go | 2 +- core/router/router_handlers_order_test.go | 70 ++++++++++++++++ middleware/rewrite/rewrite.go | 49 ++++++++++- 4 files changed, 151 insertions(+), 52 deletions(-) diff --git a/_examples/routing/subdomains/redirect/main.go b/_examples/routing/subdomains/redirect/main.go index 51562a99..132f90e7 100644 --- a/_examples/routing/subdomains/redirect/main.go +++ b/_examples/routing/subdomains/redirect/main.go @@ -1,73 +1,59 @@ -// Package main shows how to register a simple 'www' subdomain, -// using the `app.WWW` method, which will register a router wrapper which will -// redirect all 'mydomain.com' requests to 'www.mydomain.com'. -// Check the 'hosts' file to see how to test the 'mydomain.com' on your local machine. package main -import "github.com/kataras/iris/v12" - -const addr = "mydomain.com:80" +import ( + "github.com/kataras/iris/v12" + "github.com/kataras/iris/v12/middleware/rewrite" +) func main() { app := newApp() - // http(s)://mydomain.com, will be redirect to http(s)://www.mydomain.com. - // The `www` variable is the `app.Subdomain("www")`. - // - // app.WWW() wraps the router so it can redirect all incoming requests - // that comes from 'http(s)://mydomain.com/%path%' (www is missing) - // to `http(s)://www.mydomain.com/%path%`. - // - // Try: // http://mydomain.com -> http://www.mydomain.com - // http://mydomain.com/users -> http://www.mydomain.com/users - // http://mydomain.com/users/login -> http://www.mydomain.com/users/login - app.Listen(addr) + // http://mydomain.com/user -> http://www.mydomain.com/user + // http://mydomain.com/user/login -> http://www.mydomain.com/user/login + app.Listen(":80") } func newApp() *iris.Application { app := iris.New() - app.Get("/", func(ctx iris.Context) { - ctx.Writef("This will never be executed.") - }) + app.Logger().SetLevel("debug") - www := app.Subdomain("www") // <- same as app.Party("www.") - www.Get("/", index) + static := app.Subdomain("static") + static.Get("/", staticIndex) - // www is an `iris.Party`, use it like you already know, like grouping routes. - www.PartyFunc("/users", func(p iris.Party) { // <- same as www.Party("/users").Get(...) - p.Get("/", usersIndex) - p.Get("/login", getLogin) - }) + app.Get("/", index) + userRouter := app.Party("/user") + userRouter.Get("/", userGet) + userRouter.Get("/login", userGetLogin) - // redirects mydomain.com/%anypath% to www.mydomain.com/%anypath%. - // First argument is the 'from' and second is the 'to/target'. - app.SubdomainRedirect(app, www) - - // SubdomainRedirect works for multi-level subdomains as well: - // subsub := www.Subdomain("subsub") // subsub.www.mydomain.com - // subsub.Get("/", func(ctx iris.Context) { ctx.Writef("subdomain is: " + ctx.Subdomain()) }) - // app.SubdomainRedirect(subsub, www) + // redirects := rewrite.Load("redirects.yml") + // ^ see _examples/routing/rewrite example for that. // - // If you need to redirect any subdomain to 'www' then: - // app.SubdomainRedirect(app.WildcardSubdomain(), www) - // If you need to redirect from a subdomain to the root domain then: - // app.SubdomainRedirect(app.Subdomain("mysubdomain"), app) - // - // Note that app.Party("mysubdomain.") and app.Subdomain("mysubdomain") - // is the same exactly thing, the difference is that the second can omit the last dot('.'). + // Now let's do that by code. + rewriteEngine, _ := rewrite.New(rewrite.Options{ + PrimarySubdomain: "www", + }) + // Enable this line for debugging: + // rewriteEngine.SetLogger(app.Logger()) + app.WrapRouter(rewriteEngine.Rewrite) return app } +func staticIndex(ctx iris.Context) { + ctx.Writef("This is the static.mydomain.com index.") +} + func index(ctx iris.Context) { - ctx.Writef("This is the www.mydomain.com endpoint.") + ctx.Writef("This is the www.mydomain.com index.") } -func usersIndex(ctx iris.Context) { - ctx.Writef("This is the www.mydomain.com/users endpoint.") +func userGet(ctx iris.Context) { + // Also, ctx.Subdomain(), ctx.SubdomainFull(), ctx.Host() and ctx.Path() + // can be helpful when working with subdomains. + ctx.Writef("This is the www.mydomain.com/user endpoint.") } -func getLogin(ctx iris.Context) { - ctx.Writef("This is the www.mydomain.com/users/login endpoint.") +func userGetLogin(ctx iris.Context) { + ctx.Writef("This is the www.mydomain.com/user/login endpoint.") } diff --git a/core/router/api_builder.go b/core/router/api_builder.go index 2e795865..b55cf421 100644 --- a/core/router/api_builder.go +++ b/core/router/api_builder.go @@ -980,7 +980,7 @@ func (api *APIBuilder) UseOnce(handlers ...context.Handler) { // UseGlobal registers handlers that should run at the very beginning. // It prepends those handler(s) to all routes, -// including all parties, subdomains. +// including all parties, subdomains and errors. // It doesn't care about call order, it will prepend the handlers to all // existing routes and the future routes that may being registered. // diff --git a/core/router/router_handlers_order_test.go b/core/router/router_handlers_order_test.go index 5d9b5a7d..110993e8 100644 --- a/core/router/router_handlers_order_test.go +++ b/core/router/router_handlers_order_test.go @@ -8,6 +8,7 @@ package router_test import ( "fmt" + "net/http" "testing" "github.com/kataras/iris/v12" @@ -285,3 +286,72 @@ func TestUseRouterSubdomains(t *testing.T) { e.GET("/notfound").WithURL("http://old.example.com").Expect().Status(iris.StatusNotFound).Body(). Equal("Not Found") } + +func TestUseWrapOrder(t *testing.T) { + var ( + expectedBody = "#1 .WrapRouter\n#2 .UseRouter\n#3 .UseGlobal\n#4 .Use\n#5 Main Handler\n" + expectedNotFoundBody = "#3 .UseGlobal\n#1 .UseError\n#2 Main Error Handler\n" + makeMiddleware = func(body string) iris.Handler { + return func(ctx iris.Context) { + ctx.WriteString(body) + ctx.Next() + } + } + + handler = func(ctx iris.Context) { + ctx.WriteString("#5 Main Handler\n") + } + + errorHandler = func(ctx iris.Context) { + ctx.WriteString("#2 Main Error Handler\n") + } + + useHandler = makeMiddleware("#4 .Use\n") + useGlobal = makeMiddleware("#3 .UseGlobal\n") + useError = func(ctx iris.Context) { + // UseError has captured the status code, because it runs + // after the router itself but only one error handlers. + ctx.WriteString("#1 .UseError\n") + ctx.Next() + } + useRouter = func(ctx iris.Context) { + if ctx.Path() == "/" { + ctx.WriteString("#2 .UseRouter\n") + } + + ctx.Next() + } + wrapRouter = func(w http.ResponseWriter, r *http.Request, router http.HandlerFunc) { + if r.URL.Path == "/" { + w.Write([]byte("#1 .WrapRouter\n")) + // Note for beginners, reading this test: + // if we write something here on a not found page, + // in the raw net/http wrapper like this one, + // then the response writer sends 200 status OK + // (on first write) and so any error handler will not be fired as expected, + // these are basic things. If you w.WriteHeader you cannot change the status code later on too. + // In Iris handlers, if you write before status code set, then it sends 200 + // and it cannot change too (if you want to change that behavior you use ctx.Record()). + // However if you + // just call ctx.StatusCode without content written then you are able to change the status code + // later on. + } + + router(w, r) + } + ) + + app := iris.New() + app.Use(useHandler) + app.UseGlobal(useGlobal) + app.UseError(useError) + app.UseRouter(useRouter) + app.WrapRouter(wrapRouter) + + app.OnErrorCode(iris.StatusNotFound, errorHandler) + app.Get("/", handler) + + e := httptest.New(t, app) + e.GET("/NotFound").Expect().Status(iris.StatusNotFound).Body().Equal(expectedNotFoundBody) + e.GET("/").Expect().Status(iris.StatusOK).Body().Equal(expectedBody) +} diff --git a/middleware/rewrite/rewrite.go b/middleware/rewrite/rewrite.go index 6ce71ce6..0edf6345 100644 --- a/middleware/rewrite/rewrite.go +++ b/middleware/rewrite/rewrite.go @@ -13,6 +13,7 @@ import ( "github.com/kataras/iris/v12/context" "github.com/kataras/iris/v12/core/router" + "github.com/kataras/golog" "gopkg.in/yaml.v3" ) @@ -69,6 +70,7 @@ type Engine struct { redirects []*redirectMatch options Options + logger *golog.Logger domainValidator func(string) bool } @@ -117,6 +119,35 @@ func New(opts Options) (*Engine, error) { return e, nil } +// SetLogger attachs a logger to the Rewrite Engine, +// used only for debugging. +// Defaults to nil. +func (e *Engine) SetLogger(logger *golog.Logger) *Engine { + e.logger = logger.Child(e).SetChildPrefix("rewrite") + return e +} + +// init the request logging with [DBUG]. +func (e *Engine) initDebugf(format string, args ...interface{}) { + if e.logger == nil { + return + } + + e.logger.Debugf(format, args...) +} + +var skipDBUGSpace = strings.Repeat(" ", 7) + +// continue debugging the same request with new lines and spacing, +// easier to read. +func (e *Engine) debugf(format string, args ...interface{}) { + if e.logger == nil || e.logger.Level < golog.DebugLevel { + return + } + + fmt.Fprintf(e.logger.Printer, skipDBUGSpace+format, args...) +} + // Handler is an Iris Handler that can be used as a router or party or route middleware. // For a global alternative, if you want to wrap the entire Iris Application // use the `Wrapper` instead. @@ -141,6 +172,8 @@ func (e *Engine) Rewrite(w http.ResponseWriter, r *http.Request, routeHandler ht if primarySubdomain := e.options.PrimarySubdomain; primarySubdomain != "" { hostport := context.GetHost(r) root := context.GetDomain(hostport) + + e.initDebugf("Begin request: full host: %s and root domain: %s", hostport, root) // Note: // localhost and 127.0.0.1 are not supported for subdomain rewrite, by purpose, // use a virtual host instead. @@ -150,10 +183,14 @@ func (e *Engine) Rewrite(w http.ResponseWriter, r *http.Request, routeHandler ht root += getPort(hostport) subdomain := strings.TrimSuffix(hostport, root) + e.debugf("Domain is not a loopback, requested subdomain: %s\n", subdomain) + if subdomain == "" { // we are in root domain, full redirect to its primary subdomain. - r.Host = primarySubdomain + root - r.URL.Host = primarySubdomain + root + newHost := primarySubdomain + root + e.debugf("Redirecting from root domain to: %s\n", newHost) + r.Host = newHost + r.URL.Host = newHost http.Redirect(w, r, r.URL.String(), http.StatusMovedPermanently) return } @@ -164,13 +201,15 @@ func (e *Engine) Rewrite(w http.ResponseWriter, r *http.Request, routeHandler ht // to bypass the subdomain router (`routeHandler`) // do not return, redirects should be respected. rootHost := strings.TrimPrefix(hostport, subdomain) - + e.debugf("Request host field was modified to: %s. Proceed without redirection\n", rootHost) // modify those for the next redirects or the route handler. r.Host = rootHost r.URL.Host = rootHost } // maybe other subdomain or not at all, let's continue. + } else { + e.debugf("Primary subdomain is: %s but redirect response was not sent. Domain is a loopback?\n", primarySubdomain) } } @@ -183,6 +222,7 @@ func (e *Engine) Rewrite(w http.ResponseWriter, r *http.Request, routeHandler ht if target, ok := rd.matchAndReplace(src); ok { if target == src { + e.debugf("WARNING: source and target URLs match: %s\n", src) routeHandler(w, r) return } @@ -194,6 +234,7 @@ func (e *Engine) Rewrite(w http.ResponseWriter, r *http.Request, routeHandler ht return } + e.debugf("No redirect: handle request: %s as: %s\n", r.RequestURI, u) r.URL = u routeHandler(w, r) return @@ -202,8 +243,10 @@ func (e *Engine) Rewrite(w http.ResponseWriter, r *http.Request, routeHandler ht if !rd.isRelativePattern { // this performs better, no need to check query or host, // the uri already built. + e.debugf("Full redirect: from: %s to: %s\n", src, target) redirectAbs(w, r, target, rd.code) } else { + e.debugf("Path redirect: from: %s to: %s\n", src, target) http.Redirect(w, r, target, rd.code) }