1
0
mirror of https://github.com/kataras/iris.git synced 2026-01-11 05:55:57 +00:00

new apps/switch (beta)

This commit is contained in:
Gerasimos (Makis) Maropoulos
2020-08-17 21:53:17 +03:00
parent a61f743fa8
commit 589c8c6242
14 changed files with 678 additions and 48 deletions

4
apps/apps.go Normal file
View File

@@ -0,0 +1,4 @@
// Package apps is responsible to control many Iris Applications.
// This package directly imports the iris root package and cannot be used
// inside Iris' codebase itself. Only external packages/programs can make use of it.
package apps

127
apps/switch.go Normal file
View File

@@ -0,0 +1,127 @@
package apps
import (
"github.com/kataras/iris/v12"
)
// Switch returns a new Application
// with the sole purpose of routing the
// matched Applications through the provided "cases".
//
// The cases are filtered in order of register.
//
// Example:
// switcher := Switch(Hosts{
// "mydomain.com": app,
// "test.mydomain.com": testSubdomainApp,
// "otherdomain.com": "appName",
// })
// switcher.Listen(":80")
//
// Note that this is NOT a load balancer. The filters
// are executed by registration order and matched Application
// handles the request.
//
// The returned Switch Application can register routes that will run
// if no application is matched against the given filters.
// The returned Switch Application can also register custom error code handlers,
// e.g. to inject the 404 on not application found.
// It can also be wrapped with its `WrapRouter` method,
// which is really useful for logging and statistics.
func Switch(providers ...SwitchProvider) *iris.Application {
if len(providers) == 0 {
panic("iris: switch: empty providers")
}
var cases []SwitchCase
for _, p := range providers {
for _, c := range p.GetSwitchCases() {
cases = append(cases, c)
}
}
if len(cases) == 0 {
panic("iris: switch: empty cases")
}
app := iris.New()
// Try to build the cases apps on app.Build/Listen/Run so
// end-developers don't worry about it.
app.OnBuild = func() error {
for _, c := range cases {
if err := c.App.Build(); err != nil {
return err
}
}
return nil
}
// If we have a request to support
// middlewares in that switcher app then
// we can use app.Get("{p:path}"...) instead.
app.UseRouter(func(ctx iris.Context) {
for _, c := range cases {
if c.Filter(ctx) {
c.App.ServeHTTP(ctx.ResponseWriter().Naive(), ctx.Request())
// if c.App.Downgraded() {
// c.App.ServeHTTP(ctx.ResponseWriter(), ctx.Request())
// } else {
// Note(@kataras): don't ever try something like that;
// the context pool is the switcher's one.
// ctx.SetApplication(c.App)
// c.App.ServeHTTPC(ctx)
// ctx.SetApplication(app)
// }
return
}
}
// let the "switch app" handle it or fire a custom 404 error page,
// next is the switch app's router.
ctx.Next()
})
return app
}
type (
// SwitchCase contains the filter
// and the matched Application instance.
SwitchCase struct {
Filter iris.Filter
App *iris.Application
}
// A SwitchProvider should return the switch cases.
// It's an interface instead of a direct slice because
// we want to make available different type of structures
// without wrapping.
SwitchProvider interface {
GetSwitchCases() []SwitchCase
}
// Join returns a new slice which joins different type of switch cases.
Join []SwitchProvider
)
var _ SwitchProvider = SwitchCase{}
// GetSwitchCases completes the SwitchProvider, it returns itself.
func (sc SwitchCase) GetSwitchCases() []SwitchCase {
return []SwitchCase{sc}
}
var _ SwitchProvider = Join{}
// GetSwitchCases completes the switch provider.
func (j Join) GetSwitchCases() (cases []SwitchCase) {
for _, p := range j {
if p == nil {
continue
}
cases = append(cases, p.GetSwitchCases()...)
}
return
}

37
apps/switch_go_test.go Normal file
View File

@@ -0,0 +1,37 @@
package apps
import (
"fmt"
"testing"
"github.com/kataras/iris/v12"
)
func TestSwitchJoin(t *testing.T) {
myapp := iris.New()
customFilter := func(ctx iris.Context) bool {
pass, _ := ctx.URLParamBool("filter")
return pass
}
joinedCases := Join{
SwitchCase{
Filter: customFilter,
App: myapp,
},
Hosts{{Pattern: "^test.*$", Target: myapp}},
}
cases := []SwitchCase{
{
Filter: customFilter,
App: myapp,
},
{Filter: hostFilter("^test.*$"), App: myapp},
}
if expected, got := fmt.Sprintf("%#+v", cases), fmt.Sprintf("%#+v", joinedCases.GetSwitchCases()); expected != got {
t.Fatalf("join does not match with the expected slice of cases, expected:\n%s\nbut got:\n%s", expected, got)
}
}

120
apps/switch_hosts.go Normal file
View File

@@ -0,0 +1,120 @@
package apps
import (
"fmt"
"net/http"
"net/url"
"regexp"
"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
}
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, http.StatusText(iris.StatusLoopDetected), iris.StatusLoopDetected)
return
}
r.Host = targetHost
r.URL.Host = targetHost
// r.URL.User = nil
http.Redirect(w, r, r.URL.String(), code)
})
return app
}

211
apps/switch_hosts_test.go Normal file
View File

@@ -0,0 +1,211 @@
package apps
import (
"fmt"
"net/url"
"testing"
"github.com/kataras/iris/v12"
"github.com/kataras/iris/v12/context"
"github.com/kataras/iris/v12/httptest"
)
type testRequests map[string]map[string]int // url -> path -> status code
func TestSwitchHosts(t *testing.T) {
var (
expected = func(app context.Application, host string) string {
return fmt.Sprintf("App Name: %s\nHost: %s", app, host)
}
index = func(ctx iris.Context) {
ctx.WriteString(expected(ctx.Application(), ctx.Host()))
}
)
testdomain1 := iris.New().SetName("test 1 domain")
testdomain1.Get("/", index) // should match host matching with "testdomain1.com".
testdomain2 := iris.New().SetName("test 2 domain")
testdomain2.Get("/", index) // should match host matching with "testdomain2.com".
mydomain := iris.New().SetName("my domain")
mydomain.OnErrorCode(iris.StatusNotFound, func(ctx iris.Context) {
ctx.WriteString(ctx.Host() + " custom not found")
})
mydomain.Get("/", index) // should match ALL hosts starting with "my".
tests := []struct {
Pattern string
Target *iris.Application
Requests testRequests
}{
{
"testdomain1.com",
testdomain1,
testRequests{
"http://testdomain1.com": {
"/": iris.StatusOK,
},
},
},
{
"testdomain2.com",
testdomain2,
testRequests{
"http://testdomain2.com": {
"/": iris.StatusOK,
},
},
},
{
"^my.*$",
mydomain,
testRequests{
"http://mydomain.com": {
"/": iris.StatusOK,
"/nf": iris.StatusNotFound,
},
"http://myotherdomain.com": {
"/": iris.StatusOK,
},
"http://mymy.com": {
"/": iris.StatusOK,
},
"http://nmy.com": {
"/": iris.StatusBadGateway, /* 404 hijacked by switch.OnErrorCode */
},
},
},
}
var hosts Hosts
for _, tt := range tests {
hosts = append(hosts, Host{tt.Pattern, tt.Target})
}
switcher := Switch(hosts)
switcher.OnErrorCode(iris.StatusNotFound, func(ctx iris.Context) {
// inject the 404 to 502.
// tests the ctx.Next inside the Hosts switch provider.
ctx.StatusCode(iris.StatusBadGateway)
ctx.WriteString("Switcher: Bad Gateway")
})
e := httptest.New(t, switcher)
for i, tt := range tests {
for URL, paths := range tt.Requests {
u, err := url.Parse(URL)
if err != nil {
t.Fatalf("[%d] %v", i, err)
}
targetHost := u.Host
for requestPath, statusCode := range paths {
// url := fmt.Sprintf("http://%s", requestHost)
body := expected(tt.Target, targetHost)
switch statusCode {
case 404:
body = targetHost + " custom not found"
case 502:
body = "Switcher: Bad Gateway"
}
e.GET(requestPath).WithURL(URL).Expect().Status(statusCode).Body().Equal(body)
}
}
}
}
func TestSwitchHostsRedirect(t *testing.T) {
var (
expected = func(appName, host, path string) string {
return fmt.Sprintf("App Name: %s\nHost: %s\nPath: %s", appName, host, path)
}
index = func(ctx iris.Context) {
ctx.WriteString(expected(ctx.Application().String(), ctx.Host(), ctx.Path()))
}
)
mydomain := iris.New().SetName("mydomain")
mydomain.OnAnyErrorCode(func(ctx iris.Context) {
ctx.WriteString("custom: " + iris.StatusText(ctx.GetStatusCode()))
})
mydomain.Get("/", index)
mydomain.Get("/f", index)
tests := []struct {
Pattern string
Target string
Requests testRequests
}{
{
"www.mydomain.com",
"mydomain",
testRequests{
"http://www.mydomain.com": {
"/": iris.StatusOK,
"/f": iris.StatusOK,
"/nf": iris.StatusNotFound,
},
},
},
{
"^test.*$",
"mydomain",
testRequests{
"http://testdomain.com": {
"/": iris.StatusOK,
"/f": iris.StatusOK,
"/nf": iris.StatusNotFound,
},
},
},
// Something like this will panic to protect users:
// {
// ...,
// "^my.*$",
// "mydomain.com",
// ...
//
{
"^www.*$",
"google.com",
testRequests{
"http://www.mydomain.com": {
"/": iris.StatusOK,
},
"http://www.golang.org": {
"/": iris.StatusNotFound, // should give not found because this is not a switcher's web app.
},
},
},
}
var hostsRedirect Hosts
for _, tt := range tests {
hostsRedirect = append(hostsRedirect, Host{tt.Pattern, tt.Target})
}
switcher := Switch(hostsRedirect)
e := httptest.New(t, switcher)
for i, tt := range tests {
for requestURL, paths := range tt.Requests {
u, err := url.Parse(requestURL)
if err != nil {
t.Fatalf("[%d] %v", i, err)
}
targetHost := u.Host
for requestPath, statusCode := range paths {
body := expected(mydomain.String(), targetHost, requestPath)
if statusCode != 200 {
if tt.Target != mydomain.String() { // it's external.
body = "Not Found"
} else {
body = "custom: " + iris.StatusText(statusCode)
}
}
e.GET(requestPath).WithURL(requestURL).Expect().Status(statusCode).Body().Equal(body)
}
}
}
}

1
apps/switch_scheme.go Normal file
View File

@@ -0,0 +1 @@
package apps