iris/apps/switch_hosts.go

133 lines
4.0 KiB
Go

package apps
import (
"fmt"
"net/http"
"net/url"
"regexp"
"strings"
"github.com/kataras/iris/v12"
"github.com/kataras/iris/v12/context"
)
type (
// Host holds the pattern for the SwitchCase filter
// and the Target host or application.
Host struct {
// Pattern is the incoming host matcher regexp or a literal.
Pattern string
// Target is the target Host that incoming requests will be redirected on pattern match
// or an Application's Name that will handle the incoming request matched the Pattern.
Target interface{} // It was a string in my initial design but let's do that interface{}, we may support more types here in the future, until generics are in, keep it interface{}.
}
// Hosts is a switch provider.
// It can be used as input argument to the `Switch` function
// to map host to existing Iris Application instances, e.g.
// { "www.mydomain.com": "mydomainApp" } .
// It can accept regexp as a host too, e.g.
// { "^my.*$": "mydomainApp" } .
Hosts []Host
// Good by we need order and map can't provide it for us
// (e.g. "fallback" regexp }
// Hosts map[string]*iris.Application
)
var _ SwitchProvider = Hosts{}
// GetSwitchCases completes the SwitchProvider.
// It returns a slice of SwitchCase which
// if passed on `Switch` function, they act
// as a router between matched domains and subdomains
// between existing Iris Applications.
func (hosts Hosts) GetSwitchCases() []SwitchCase {
cases := make([]SwitchCase, 0, len(hosts))
for _, host := range hosts {
cases = append(cases, SwitchCase{
Filter: hostFilter(host.Pattern),
App: hostApp(host),
})
}
return cases
}
// GetFriendlyName implements the FriendlyNameProvider.
func (hosts Hosts) GetFriendlyName() string {
var patterns []string
for _, host := range hosts {
if strings.TrimSpace(host.Pattern) != "" {
patterns = append(patterns, host.Pattern)
}
}
return strings.Join(patterns, ", ")
}
func hostApp(host Host) *iris.Application {
if host.Target == nil {
return nil
}
switch target := host.Target.(type) {
case context.Application:
return target.(*iris.Application)
case string:
// Check if the given target is an application name, if so
// we must not redirect (loop) we must serve the request
// using that app.
if targetApp, ok := context.GetApplication(target); ok {
// It's always iris.Application so we are totally safe here.
return targetApp.(*iris.Application)
}
// If it's a real host, warn the user of invalid input.
u, err := url.Parse(target)
if err == nil && u.IsAbs() {
// remember, we redirect hosts, not full URLs here.
panic(fmt.Sprintf(`iris: switch: hosts: invalid target host: "%s"`, target))
}
if regex := regexp.MustCompile(host.Pattern); regex.MatchString(target) {
panic(fmt.Sprintf(`iris: switch: hosts: loop detected between expression: "%s" and target host: "%s"`, host.Pattern, host.Target))
}
return newHostRedirectApp(target, HostsRedirectCode)
default:
panic(fmt.Sprintf("iris: switch: hosts: invalid target type: %T", target))
}
}
func hostFilter(expr string) iris.Filter {
regex := regexp.MustCompile(expr)
return func(ctx iris.Context) bool {
return regex.MatchString(ctx.Host())
}
}
// HostsRedirectCode is the default status code is used
// to redirect a matching host to a url.
var HostsRedirectCode = iris.StatusMovedPermanently
func newHostRedirectApp(targetHost string, code int) *iris.Application {
app := iris.New()
app.Downgrade(func(w http.ResponseWriter, r *http.Request) {
if targetHost == context.GetHost(r) {
// Note(@kataras):
// this should never happen as the HostsRedirect
// carefully checks if the expression already matched the "redirectTo"
// to avoid the redirect loops at all.
// iris: switch: hosts redirect: loop detected between expression: "^my.*$" and target host: "mydomain.com"
http.Error(w, iris.StatusText(iris.StatusTooManyRequests), iris.StatusTooManyRequests)
return
}
r.Host = targetHost
r.URL.Host = targetHost
// r.URL.User = nil
http.Redirect(w, r, r.URL.String(), code)
})
return app
}