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:
4
apps/apps.go
Normal file
4
apps/apps.go
Normal 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
127
apps/switch.go
Normal 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
37
apps/switch_go_test.go
Normal 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
120
apps/switch_hosts.go
Normal 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
211
apps/switch_hosts_test.go
Normal 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
1
apps/switch_scheme.go
Normal file
@@ -0,0 +1 @@
|
||||
package apps
|
||||
Reference in New Issue
Block a user