mirror of
https://github.com/kataras/iris.git
synced 2025-03-13 21:36:28 +01:00
rewrite middleware: add PrimarySubdomain and simplify its usage example
This commit is contained in:
parent
12737c5b7f
commit
ffae9c0d09
3
_examples/routing/rewrite/hosts
Normal file
3
_examples/routing/rewrite/hosts
Normal file
|
@ -0,0 +1,3 @@
|
|||
127.0.0.1 mydomain.com
|
||||
127.0.0.1 www.mydomain.com
|
||||
127.0.0.1 test.mydomain.com
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -6,3 +6,5 @@ RedirectMatch:
|
|||
- 301 /docs/v12(.*) /docs
|
||||
# redirects /old(.*) to /
|
||||
- 301 /old(.*) /
|
||||
# redirects root domain to www.
|
||||
PrimarySubdomain: www
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 ""
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user