From ffae9c0d09546cb1022c9ebe778400a403d20fd3 Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Thu, 20 Aug 2020 03:05:47 +0300 Subject: [PATCH] rewrite middleware: add PrimarySubdomain and simplify its usage example --- _examples/routing/rewrite/hosts | 3 + _examples/routing/rewrite/main.go | 71 ++++----- _examples/routing/rewrite/redirects.yml | 2 + context/context.go | 3 +- core/netutil/addr.go | 15 +- middleware/rewrite/rewrite.go | 184 +++++++++++++++--------- 6 files changed, 167 insertions(+), 111 deletions(-) create mode 100644 _examples/routing/rewrite/hosts diff --git a/_examples/routing/rewrite/hosts b/_examples/routing/rewrite/hosts new file mode 100644 index 00000000..f135219c --- /dev/null +++ b/_examples/routing/rewrite/hosts @@ -0,0 +1,3 @@ +127.0.0.1 mydomain.com +127.0.0.1 www.mydomain.com +127.0.0.1 test.mydomain.com \ No newline at end of file diff --git a/_examples/routing/rewrite/main.go b/_examples/routing/rewrite/main.go index aeb12502..448043e9 100644 --- a/_examples/routing/rewrite/main.go +++ b/_examples/routing/rewrite/main.go @@ -7,45 +7,23 @@ import ( func main() { app := iris.New() - - /* - rewriteOptions := rewrite.Options{ - RedirectMatch: []string{ - "301 /seo/(.*) /$1", - "301 /docs/v12(.*) /docs", - "301 /old(.*) /", - }} - OR Load from file: - */ - rewriteOptions := rewrite.LoadOptions("redirects.yml") - rewriteEngine, err := rewrite.New(rewriteOptions) - if err != nil { // reports any line parse errors. - app.Logger().Fatal(err) - } - app.Get("/", index) app.Get("/about", about) app.Get("/docs", docs) - /* - // To use it per-party, even if not route match: - app.UseRouter(rewriteEngine.Handler) - // To use it per-party when route match: - app.Use(rewriteEngine.Handler) - // - // To use it on a single route just pass it to the Get/Post method. - // To make the entire application respect the rewrite rules - // you have to wrap the Iris Router and pass the Wrapper method instead, - // (recommended way to use this middleware, right before Listen/Run): - */ - app.WrapRouter(rewriteEngine.Wrapper) + app.Subdomain("test").Get("/", testIndex) - // http://localhost:8080/seo + redirects := rewrite.Load("redirects.yml") + app.WrapRouter(redirects) + + // http://mydomain.com:8080/seo/about -> http://www.mydomain.com:8080/about + // http://test.mydomain.com:8080 + // http://localhost:8080/seo -> http://localhost:8080 // http://localhost:8080/about - // http://localhost:8080/docs/v12/hello - // http://localhost:8080/docs/v12some - // http://localhost:8080/oldsome - // http://localhost:8080/oldindex/random + // http://localhost:8080/docs/v12/hello -> http://localhost:8080/docs + // http://localhost:8080/docs/v12some -> http://localhost:8080/docs + // http://localhost:8080/oldsome -> http://localhost:8080 + // http://localhost:8080/oldindex/random -> http://localhost:8080 app.Listen(":8080") } @@ -60,3 +38,30 @@ func about(ctx iris.Context) { func docs(ctx iris.Context) { ctx.WriteString("Docs") } + +func testIndex(ctx iris.Context) { + ctx.WriteString("Test Subdomain Index") +} + +/* More... +rewriteOptions := rewrite.Options{ + RedirectMatch: []string{ + "301 /seo/(.*) /$1", + "301 /docs/v12(.*) /docs", + "301 /old(.*) /", + }, + PrimarySubdomain: "www", +} +rewriteEngine, err := rewrite.New(rewriteOptions) + +// To use it per-party use its `Handler` method. Even if not route match: +app.UseRouter(rewriteEngine.Handler) +// To use it per-party when route match: +app.Use(rewriteEngine.Handler) +// +// To use it on a single route just pass it to the Get/Post method. +// +// To make the entire application respect the redirect rules +// you have to wrap the Iris Router and pass the `Rewrite` method instead +// as we did at this example. +*/ diff --git a/_examples/routing/rewrite/redirects.yml b/_examples/routing/rewrite/redirects.yml index f7cb1e5c..86a8ec90 100644 --- a/_examples/routing/rewrite/redirects.yml +++ b/_examples/routing/rewrite/redirects.yml @@ -6,3 +6,5 @@ RedirectMatch: - 301 /docs/v12(.*) /docs # redirects /old(.*) to / - 301 /old(.*) / +# redirects root domain to www. +PrimarySubdomain: www diff --git a/context/context.go b/context/context.go index b2dc3b49..2cd88fc2 100644 --- a/context/context.go +++ b/context/context.go @@ -889,7 +889,8 @@ func GetDomain(hostport string) string { } switch host { - case "127.0.0.1", "0.0.0.0", "::1", "[::1]", "0:0:0:0:0:0:0:0", "0:0:0:0:0:0:0:1": + // We could use the netutil.LoopbackRegex but leave it as it's for now, it's faster. + case "localhost", "127.0.0.1", "0.0.0.0", "::1", "[::1]", "0:0:0:0:0:0:0:0", "0:0:0:0:0:0:0:1": // loopback. return "localhost" default: diff --git a/core/netutil/addr.go b/core/netutil/addr.go index 78ce80c6..12db4e74 100644 --- a/core/netutil/addr.go +++ b/core/netutil/addr.go @@ -8,14 +8,13 @@ import ( ) var ( - loopbackRegex *regexp.Regexp - loopbackSubRegex *regexp.Regexp + // LoopbackRegex the regex if matched a host:port is a loopback. + LoopbackRegex = regexp.MustCompile(`^localhost$|^127(?:\.[0-9]+){0,2}\.[0-9]+$|^(?:0*\:)*?:?0*1$`) + loopbackSubRegex = regexp.MustCompile(`^127(?:\.[0-9]+){0,2}\.[0-9]+$|^(?:0*\:)*?:?0*1$`) machineHostname string ) func init() { - loopbackRegex, _ = regexp.Compile(`^localhost$|^127(?:\.[0-9]+){0,2}\.[0-9]+$|^(?:0*\:)*?:?0*1$`) - loopbackSubRegex, _ = regexp.Compile(`^127(?:\.[0-9]+){0,2}\.[0-9]+$|^(?:0*\:)*?:?0*1$`) machineHostname, _ = os.Hostname() } @@ -46,9 +45,9 @@ func GetLoopbackSubdomain(s string) string { } // IsLoopbackHost tries to catch the local addresses when a developer -// navigates to a subdomain that its hostname differs from Application.Config.Addr. +// navigates to a subdomain that its hostname differs from Application.Configuration.VHost. // Developer may want to override this function to return always false -// in order to not allow different hostname from Application.Config.Addr in local environment (remote is not reached). +// in order to not allow different hostname from Application.Configuration.VHost in local environment (remote is not reached). var IsLoopbackHost = func(requestHost string) bool { // this func will be called if we have a subdomain actually, not otherwise, so we are // safe to do some hacks. @@ -60,7 +59,7 @@ var IsLoopbackHost = func(requestHost string) bool { // find the first index of [:]8080 or [/]mypath or nothing(root with loopback address like 127.0.0.1) // remember: we are not looking for .com or these things, if is up and running then the developer - // would probably not want to reach the server with different Application.Config.Addr than + // would probably not want to reach the server with different Application.Configuration.VHost than // he/she declared. portOrPathIdx := strings.LastIndexByte(requestHost, ':') @@ -92,7 +91,7 @@ var IsLoopbackHost = func(requestHost string) bool { // so it shouldn't hurt so much, but we don't care a lot because it's a special case here // because this function will be called only if developer him/herself can reach the server // with a loopback/local address, so we are totally safe. - valid := loopbackRegex.MatchString(hostname) + valid := LoopbackRegex.MatchString(hostname) if !valid { // if regex failed to match it, then try with the pc's name. valid = hostname == machineHostname } diff --git a/middleware/rewrite/rewrite.go b/middleware/rewrite/rewrite.go index 524aa724..3cde921e 100644 --- a/middleware/rewrite/rewrite.go +++ b/middleware/rewrite/rewrite.go @@ -10,30 +10,86 @@ import ( "strings" "github.com/kataras/iris/v12/context" + "github.com/kataras/iris/v12/core/router" "gopkg.in/yaml.v3" ) // Options holds the developer input to customize // the redirects for the Rewrite Engine. -// Look the `New` package-level function. +// Look the `New` and `Load` package-level functions. type Options struct { // RedirectMatch accepts a slice of lines // of form: // REDIRECT_CODE PATH_PATTERN TARGET_PATH // Example: []{"301 /seo/(.*) /$1"}. RedirectMatch []string `json:"redirectMatch" yaml:"RedirectMatch"` + + // Root domain requests redirect automatically to primary subdomain. + // Example: "www" to redirect always to www. + // Note that you SHOULD NOT create a www subdomain inside the Iris Application. + // This field takes care of it for you, the root application instance + // will be used to serve the requests. + PrimarySubdomain string `json:"primarySubdomain" yaml:"PrimarySubdomain"` +} + +// LoadOptions loads rewrite Options from a system file. +func LoadOptions(filename string) (opts Options) { + ext := ".yml" + if index := strings.LastIndexByte(filename, '.'); index > 1 && len(filename)-1 > index { + ext = filename[index:] + } + + f, err := os.Open(filename) + if err != nil { + panic("iris: rewrite: " + err.Error()) + } + defer f.Close() + + switch ext { + case ".yaml", ".yml": + err = yaml.NewDecoder(f).Decode(&opts) + case ".json": + err = json.NewDecoder(f).Decode(&opts) + default: + panic("iris: rewrite: unexpected file extension: " + filename) + } + + if err != nil { + panic("iris: rewrite: decode: " + err.Error()) + } + + return } // Engine is the rewrite engine master structure. // Navigate through _examples/routing/rewrite for more. type Engine struct { redirects []*redirectMatch + options Options + + domainValidator func(string) bool +} + +// Load decodes the "filename" options +// and returns a new Rewrite Engine Router Wrapper. +// It panics on errors. +// Usage: +// redirects := Load("redirects.yml") +// app.WrapRouter(redirects) +// See `New` too. +func Load(filename string) router.WrapperFunc { + opts := LoadOptions(filename) + engine, err := New(opts) + if err != nil { + panic(err) + } + return engine.Rewrite } // New returns a new Rewrite Engine based on "opts". // It reports any parser error. -// See its `Handler` or `Wrapper` methods. Depending +// See its `Handler` or `Rewrite` methods. Depending // on the needs, select one. func New(opts Options) (*Engine, error) { redirects := make([]*redirectMatch, 0, len(opts.RedirectMatch)) @@ -46,66 +102,77 @@ func New(opts Options) (*Engine, error) { redirects = append(redirects, r) } + if opts.PrimarySubdomain != "" && !strings.HasSuffix(opts.PrimarySubdomain, ".") { + opts.PrimarySubdomain += "." // www -> www. + } + e := &Engine{ + options: opts, redirects: redirects, + domainValidator: func(root string) bool { + return !strings.HasSuffix(root, localhost) + }, } return e, nil } -// Handler returns a new rewrite Iris Handler. -// It panics on any error. -// Same as engine, _ := New(opts); engine.Handler. -// Usage: -// app.UseRouter(Handler(opts)). -func Handler(opts Options) context.Handler { - engine, err := New(opts) - if err != nil { - panic(err) - } - return engine.Handler -} - // 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. // Usage: // app.UseRouter(engine.Handler) func (e *Engine) Handler(ctx *context.Context) { - // We could also do that: - // but we don't. - // e.WrapRouter(ctx.ResponseWriter(), ctx.Request(), func(http.ResponseWriter, *http.Request) { - // ctx.Next() - // }) - for _, rd := range e.redirects { - src := ctx.Path() - if !rd.isRelativePattern { - src = ctx.Request().URL.String() - } - - if target, ok := rd.matchAndReplace(src); ok { - if target == src { - // this should never happen: StatusTooManyRequests. - // keep the router flow. - ctx.Next() - return - } - - ctx.Redirect(target, rd.code) - return - } - } - - ctx.Next() + e.Rewrite(ctx.ResponseWriter(), ctx.Request(), func(http.ResponseWriter, *http.Request) { + ctx.Next() + }) } -// Wrapper wraps the entire Iris Router. -// Wrapper is a bit faster than Handler because it's executed +const localhost = "localhost" + +// Rewrite is used to wrap the entire Iris Router. +// Rewrite is a bit faster than Handler because it's executed // even before any route matched and it stops on redirect pattern match. // Use it to wrap the entire Iris Application, otherwise look `Handler` instead. // // Usage: -// app.WrapRouter(engine.Wrapper). -func (e *Engine) Wrapper(w http.ResponseWriter, r *http.Request, routeHandler http.HandlerFunc) { +// app.WrapRouter(engine.Rewrite). +func (e *Engine) Rewrite(w http.ResponseWriter, r *http.Request, routeHandler http.HandlerFunc) { + if primarySubdomain := e.options.PrimarySubdomain; primarySubdomain != "" { + hostport := context.GetHost(r) + root := context.GetDomain(hostport) + // Note: + // localhost and 127.0.0.1 are not supported for subdomain rewrite, by purpose, + // use a virtual host instead. + // GetDomain will return will return localhost or www.localhost + // on expected loopbacks. + if e.domainValidator(root) { + root += getPort(hostport) + subdomain := strings.TrimSuffix(hostport, root) + + if subdomain == "" { + // we are in root domain, full redirect to its primary subdomain. + r.Host = primarySubdomain + root + r.URL.Host = primarySubdomain + root + http.Redirect(w, r, r.URL.String(), http.StatusMovedPermanently) + return + } + + if subdomain == primarySubdomain { + // keep root domain as the Host field inside the next handlers, + // for consistently use and + // to bypass the subdomain router (`routeHandler`) + // do not return, redirects should be respected. + rootHost := strings.TrimPrefix(hostport, subdomain) + + // 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. + } + } + for _, rd := range e.redirects { src := r.URL.Path if !rd.isRelativePattern { @@ -189,31 +256,10 @@ func isDigit(ch rune) bool { return '0' <= ch && ch <= '9' } -// LoadOptions loads rewrite Options from a system file. -func LoadOptions(filename string) (opts Options) { - ext := ".yml" - if index := strings.LastIndexByte(filename, '.'); index > 1 && len(filename)-1 > index { - ext = filename[index:] +func getPort(hostport string) string { // returns :port, note that this is only called on non-loopbacks. + if portIdx := strings.IndexByte(hostport, ':'); portIdx > 0 { + return hostport[portIdx:] } - f, err := os.Open(filename) - if err != nil { - panic("iris: rewrite: " + err.Error()) - } - defer f.Close() - - switch ext { - case ".yaml", ".yml": - err = yaml.NewDecoder(f).Decode(&opts) - case ".json": - err = json.NewDecoder(f).Decode(&opts) - default: - panic("iris: rewrite: unexpected file extension: " + filename) - } - - if err != nil { - panic("iris: rewrite: decode: " + err.Error()) - } - - return + return "" }