mirror of
https://github.com/kataras/iris.git
synced 2025-01-23 10:41:03 +01:00
new apps/switch (beta)
This commit is contained in:
parent
a61f743fa8
commit
589c8c6242
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,5 +1,6 @@
|
||||||
.vscode
|
.vscode
|
||||||
.directory
|
.directory
|
||||||
|
coverage.out
|
||||||
package-lock.json
|
package-lock.json
|
||||||
go.sum
|
go.sum
|
||||||
node_modules
|
node_modules
|
||||||
|
|
11
.travis.yml
11
.travis.yml
|
@ -5,6 +5,8 @@ os:
|
||||||
- linux
|
- linux
|
||||||
- osx
|
- osx
|
||||||
go:
|
go:
|
||||||
|
# We support the latest two major Go versions:
|
||||||
|
# https://golang.org/doc/devel/release.html
|
||||||
- 1.14.x
|
- 1.14.x
|
||||||
- 1.15.x
|
- 1.15.x
|
||||||
# - master
|
# - master
|
||||||
|
@ -12,6 +14,15 @@ go_import_path: github.com/kataras/iris/v12
|
||||||
env:
|
env:
|
||||||
global:
|
global:
|
||||||
- GO111MODULE=on
|
- GO111MODULE=on
|
||||||
|
addons:
|
||||||
|
hosts:
|
||||||
|
- mydomain.com
|
||||||
|
- www.mydomain.com
|
||||||
|
- myotherdomain.com
|
||||||
|
- mymy.com
|
||||||
|
- testdomain.com
|
||||||
|
- testdomain1.com
|
||||||
|
- testdomain2.com
|
||||||
install:
|
install:
|
||||||
- go get ./...
|
- go get ./...
|
||||||
script:
|
script:
|
||||||
|
|
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
|
|
@ -84,9 +84,30 @@ type Application interface {
|
||||||
//
|
//
|
||||||
// Order may change.
|
// Order may change.
|
||||||
FindClosestPaths(subdomain, searchPath string, n int) []string
|
FindClosestPaths(subdomain, searchPath string, n int) []string
|
||||||
|
|
||||||
|
// String returns the Application's Name.
|
||||||
|
String() string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Notes(@kataras):
|
||||||
|
// Alternative places...
|
||||||
|
// 1. in apps/store, but it would require an empty `import _ "....apps/store"
|
||||||
|
// from end-developers, to avoid the import cycle and *iris.Application access.
|
||||||
|
// 2. in root package level, that could be the best option, it has access to the *iris.Application
|
||||||
|
// instead of the context.Application interface, but we try to keep the root package
|
||||||
|
// as minimum as possible, however: if in the future, those Application instances
|
||||||
|
// can be registered through network instead of same-process then we must think of that choice.
|
||||||
|
// 3. this is the best possible place, as the root package and all subpackages
|
||||||
|
// have access to this context package without import cycles and they already using it,
|
||||||
|
// the only downside is that we don't have access to the *iris.Application instance
|
||||||
|
// but this context.Application is designed that way that can execute all important methods
|
||||||
|
// as the whole Iris code base is so well written.
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
// registerApps holds all the created iris Applications by this process.
|
||||||
|
// It's slice instead of map because if IRIS_APP_NAME env var exists,
|
||||||
|
// by-default all applications running on the same machine
|
||||||
|
// will have the same name unless `Application.SetName` is called.
|
||||||
registeredApps []Application
|
registeredApps []Application
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
)
|
)
|
||||||
|
@ -107,8 +128,8 @@ func RegisterApplication(app Application) {
|
||||||
// use `Context.Application()` instead.
|
// use `Context.Application()` instead.
|
||||||
func LastApplication() Application {
|
func LastApplication() Application {
|
||||||
mu.RLock()
|
mu.RLock()
|
||||||
if n := len(registeredApps); n > 0 {
|
for i := len(registeredApps) - 1; i >= 0; i-- {
|
||||||
if app := registeredApps[n-1]; app != nil {
|
if app := registeredApps[i]; app != nil {
|
||||||
mu.RUnlock()
|
mu.RUnlock()
|
||||||
return app
|
return app
|
||||||
}
|
}
|
||||||
|
@ -117,6 +138,34 @@ func LastApplication() Application {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetApplication returns a registered Application
|
||||||
|
// based on its name. If the "appName" is not unique
|
||||||
|
// across Applications, then it will return the newest one.
|
||||||
|
func GetApplication(appName string) (Application, bool) {
|
||||||
|
mu.RLock()
|
||||||
|
|
||||||
|
for i := len(registeredApps) - 1; i >= 0; i-- {
|
||||||
|
if app := registeredApps[i]; app != nil && app.String() == appName {
|
||||||
|
mu.RUnlock()
|
||||||
|
return app, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mu.RUnlock()
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustGetApplication same as `GetApplication` but it
|
||||||
|
// panics if "appName" is not a registered Application's name.
|
||||||
|
func MustGetApplication(appName string) Application {
|
||||||
|
app, ok := GetApplication(appName)
|
||||||
|
if !ok || app == nil {
|
||||||
|
panic(appName + " is not a registered Application")
|
||||||
|
}
|
||||||
|
|
||||||
|
return app
|
||||||
|
}
|
||||||
|
|
||||||
// DefaultLogger returns a Logger instance for an Iris module.
|
// DefaultLogger returns a Logger instance for an Iris module.
|
||||||
// If the program contains at least one registered Iris Application
|
// If the program contains at least one registered Iris Application
|
||||||
// before this call then it will return a child of that Application's Logger
|
// before this call then it will return a child of that Application's Logger
|
||||||
|
|
|
@ -135,6 +135,14 @@ func NewContext(app Application) *Context {
|
||||||
return &Context{app: app}
|
return &Context{app: app}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Not required, unless requested.
|
||||||
|
// SetApplication sets an Iris Application on-fly.
|
||||||
|
// Do NOT use it after ServeHTTPC is fired.
|
||||||
|
func (ctx *Context) SetApplication(app Application) {
|
||||||
|
ctx.app = app
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
// Clone returns a copy of the context that
|
// Clone returns a copy of the context that
|
||||||
// can be safely used outside the request's scope.
|
// can be safely used outside the request's scope.
|
||||||
// Note that if the request-response lifecycle terminated
|
// Note that if the request-response lifecycle terminated
|
||||||
|
@ -898,8 +906,22 @@ func (ctx *Context) Domain() string {
|
||||||
return GetDomain(ctx.Host())
|
return GetDomain(ctx.Host())
|
||||||
}
|
}
|
||||||
|
|
||||||
// SubdomainFull returnst he full subdomain level, e.g.
|
// GetSubdomainFull returns the full subdomain level, e.g.
|
||||||
// [test.user.]mydomain.com.
|
// [test.user.]mydomain.com.
|
||||||
|
func GetSubdomainFull(r *http.Request) string {
|
||||||
|
host := GetHost(r) // host:port
|
||||||
|
rootDomain := GetDomain(host) // mydomain.com
|
||||||
|
rootDomainIdx := strings.Index(host, rootDomain)
|
||||||
|
if rootDomainIdx == -1 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return host[0:rootDomainIdx]
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubdomainFull returns the full subdomain level, e.g.
|
||||||
|
// [test.user.]mydomain.com.
|
||||||
|
// Note that HostProxyHeaders are being respected here.
|
||||||
func (ctx *Context) SubdomainFull() string {
|
func (ctx *Context) SubdomainFull() string {
|
||||||
host := ctx.Host() // host:port
|
host := ctx.Host() // host:port
|
||||||
rootDomain := GetDomain(host) // mydomain.com
|
rootDomain := GetDomain(host) // mydomain.com
|
||||||
|
|
|
@ -19,7 +19,7 @@ var (
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
handlerNames = make(map[*nameExpr]string)
|
handlerNames = make(map[*NameExpr]string)
|
||||||
handlerNamesMu sync.RWMutex
|
handlerNamesMu sync.RWMutex
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -44,19 +44,22 @@ func SetHandlerName(original string, replacement string) {
|
||||||
// when a handler name is declared as it's and cause regex parsing expression error,
|
// when a handler name is declared as it's and cause regex parsing expression error,
|
||||||
// e.g. `iris/cache/client.(*Handler).ServeHTTP-fm`
|
// e.g. `iris/cache/client.(*Handler).ServeHTTP-fm`
|
||||||
regex, _ := regexp.Compile(original)
|
regex, _ := regexp.Compile(original)
|
||||||
handlerNames[&nameExpr{
|
handlerNames[&NameExpr{
|
||||||
literal: original,
|
literal: original,
|
||||||
regex: regex,
|
regex: regex,
|
||||||
}] = replacement
|
}] = replacement
|
||||||
handlerNamesMu.Unlock()
|
handlerNamesMu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
type nameExpr struct {
|
// NameExpr regex or literal comparison through `MatchString`.
|
||||||
|
type NameExpr struct {
|
||||||
regex *regexp.Regexp
|
regex *regexp.Regexp
|
||||||
literal string
|
literal string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (expr *nameExpr) MatchString(s string) bool {
|
// MatchString reports whether "s" is literal of "literal"
|
||||||
|
// or it matches the regex expression at "regex".
|
||||||
|
func (expr *NameExpr) MatchString(s string) bool {
|
||||||
if expr.literal == s { // if matches as string, as it's.
|
if expr.literal == s { // if matches as string, as it's.
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
|
@ -492,6 +492,9 @@ func (su *Supervisor) RegisterOnShutdown(cb func()) {
|
||||||
// for them to close, if desired.
|
// for them to close, if desired.
|
||||||
func (su *Supervisor) Shutdown(ctx context.Context) error {
|
func (su *Supervisor) Shutdown(ctx context.Context) error {
|
||||||
atomic.StoreUint32(&su.closedManually, 1) // future-use
|
atomic.StoreUint32(&su.closedManually, 1) // future-use
|
||||||
|
if ctx == nil {
|
||||||
|
ctx = context.Background()
|
||||||
|
}
|
||||||
return su.Server.Shutdown(ctx)
|
return su.Server.Shutdown(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -169,27 +169,6 @@ func (s *subdomainRedirectWrapper) Wrapper(w http.ResponseWriter, r *http.Reques
|
||||||
router(w, r)
|
router(w, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
func redirectAbsolute(w http.ResponseWriter, r *http.Request, url string, code int) {
|
|
||||||
h := w.Header()
|
|
||||||
|
|
||||||
// RFC 7231 notes that a short HTML body is usually included in
|
|
||||||
// the response because older user agents may not understand 301/307.
|
|
||||||
// Do it only if the request didn't already have a Content-Type header.
|
|
||||||
_, hadCT := h[context.ContentTypeHeaderKey]
|
|
||||||
|
|
||||||
h.Set("Location", url)
|
|
||||||
if !hadCT && (r.Method == http.MethodGet || r.Method == http.MethodHead) {
|
|
||||||
h.Set(context.ContentTypeHeaderKey, "text/html; charset=utf-8")
|
|
||||||
}
|
|
||||||
w.WriteHeader(code)
|
|
||||||
|
|
||||||
// Shouldn't send the body for POST or HEAD; that leaves GET.
|
|
||||||
if !hadCT && r.Method == "GET" {
|
|
||||||
body := "<a href=\"" + template.HTMLEscapeString(url) + "\">" + http.StatusText(code) + "</a>.\n"
|
|
||||||
fmt.Fprintln(w, body)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewSubdomainPartyRedirectHandler returns a handler which can be registered
|
// NewSubdomainPartyRedirectHandler returns a handler which can be registered
|
||||||
// through `UseRouter` or `Use` to redirect from the current request's
|
// through `UseRouter` or `Use` to redirect from the current request's
|
||||||
// subdomain to the one which the given `to` Party can handle.
|
// subdomain to the one which the given `to` Party can handle.
|
||||||
|
@ -210,9 +189,34 @@ func NewSubdomainRedirectHandler(toSubdomain string) context.Handler {
|
||||||
// en-us.test.mydomain.com
|
// en-us.test.mydomain.com
|
||||||
host := ctx.Host()
|
host := ctx.Host()
|
||||||
fullSubdomain := ctx.SubdomainFull()
|
fullSubdomain := ctx.SubdomainFull()
|
||||||
newHost := strings.Replace(host, fullSubdomain, toSubdomain, 1)
|
targetHost := strings.Replace(host, fullSubdomain, toSubdomain, 1)
|
||||||
resturi := ctx.Request().URL.RequestURI()
|
// resturi := ctx.Request().URL.RequestURI()
|
||||||
urlToRedirect := ctx.Scheme() + newHost + resturi
|
// urlToRedirect := ctx.Scheme() + newHost + resturi
|
||||||
redirectAbsolute(ctx.ResponseWriter(), ctx.Request(), urlToRedirect, http.StatusMovedPermanently)
|
r := ctx.Request()
|
||||||
|
r.Host = targetHost
|
||||||
|
r.URL.Host = targetHost
|
||||||
|
urlToRedirect := r.URL.String()
|
||||||
|
redirectAbsolute(ctx.ResponseWriter(), r, urlToRedirect, http.StatusMovedPermanently)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func redirectAbsolute(w http.ResponseWriter, r *http.Request, url string, code int) {
|
||||||
|
h := w.Header()
|
||||||
|
|
||||||
|
// RFC 7231 notes that a short HTML body is usually included in
|
||||||
|
// the response because older user agents may not understand 301/307.
|
||||||
|
// Do it only if the request didn't already have a Content-Type header.
|
||||||
|
_, hadCT := h[context.ContentTypeHeaderKey]
|
||||||
|
|
||||||
|
h.Set("Location", url)
|
||||||
|
if !hadCT && (r.Method == http.MethodGet || r.Method == http.MethodHead) {
|
||||||
|
h.Set(context.ContentTypeHeaderKey, "text/html; charset=utf-8")
|
||||||
|
}
|
||||||
|
w.WriteHeader(code)
|
||||||
|
|
||||||
|
// Shouldn't send the body for POST or HEAD; that leaves GET.
|
||||||
|
if !hadCT && r.Method == "GET" {
|
||||||
|
body := "<a href=\"" + template.HTMLEscapeString(url) + "\">" + http.StatusText(code) + "</a>.\n"
|
||||||
|
fmt.Fprintln(w, body)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
69
iris.go
69
iris.go
|
@ -82,8 +82,20 @@ type Application struct {
|
||||||
// used for build
|
// used for build
|
||||||
builded bool
|
builded bool
|
||||||
defaultMode bool
|
defaultMode bool
|
||||||
|
// OnBuild is a single function which
|
||||||
|
// is fired on the first `Build` method call.
|
||||||
|
// If reports an error then the execution
|
||||||
|
// is stopped and the error is logged.
|
||||||
|
// It's nil by default except when `Switch` instead of `New` or `Default`
|
||||||
|
// is used to initialize the Application.
|
||||||
|
// Users can wrap it to accept more events.
|
||||||
|
OnBuild func() error
|
||||||
|
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
|
// name is the application name and the log prefix for
|
||||||
|
// that Application instance's Logger. See `SetName` and `String`.
|
||||||
|
// Defaults to IRIS_APP_NAME envrinoment variable otherwise empty.
|
||||||
|
name string
|
||||||
// Hosts contains a list of all servers (Host Supervisors) that this app is running on.
|
// Hosts contains a list of all servers (Host Supervisors) that this app is running on.
|
||||||
//
|
//
|
||||||
// Hosts may be empty only if application ran(`app.Run`) with `iris.Raw` option runner,
|
// Hosts may be empty only if application ran(`app.Run`) with `iris.Raw` option runner,
|
||||||
|
@ -131,6 +143,41 @@ func Default() *Application {
|
||||||
return app
|
return app
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newLogger(app *Application) *golog.Logger {
|
||||||
|
logger := golog.Default.Child(app)
|
||||||
|
if name := os.Getenv("IRIS_APP_NAME"); name != "" {
|
||||||
|
app.name = name
|
||||||
|
logger.SetChildPrefix(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetName sets a unique name to this Iris Application.
|
||||||
|
// It sets a child prefix for the current Application's Logger.
|
||||||
|
// Look `String` method too.
|
||||||
|
//
|
||||||
|
// It returns this Application.
|
||||||
|
func (app *Application) SetName(appName string) *Application {
|
||||||
|
app.mu.Lock()
|
||||||
|
defer app.mu.Unlock()
|
||||||
|
|
||||||
|
if app.name == "" {
|
||||||
|
app.logger.SetChildPrefix(appName)
|
||||||
|
}
|
||||||
|
app.name = appName
|
||||||
|
|
||||||
|
return app
|
||||||
|
}
|
||||||
|
|
||||||
|
// String completes the fmt.Stringer interface and it returns
|
||||||
|
// the application's name.
|
||||||
|
// If name was not set by `SetName` or `IRIS_APP_NAME` environment variable
|
||||||
|
// then this will return an empty string.
|
||||||
|
func (app *Application) String() string {
|
||||||
|
return app.name
|
||||||
|
}
|
||||||
|
|
||||||
// WWW creates and returns a "www." subdomain.
|
// WWW creates and returns a "www." subdomain.
|
||||||
// The difference from `app.Subdomain("www")` or `app.Party("www.")` is that the `app.WWW()` method
|
// The difference from `app.Subdomain("www")` or `app.Party("www.")` is that the `app.WWW()` method
|
||||||
// wraps the router so all http(s)://mydomain.com will be redirect to http(s)://www.mydomain.com.
|
// wraps the router so all http(s)://mydomain.com will be redirect to http(s)://www.mydomain.com.
|
||||||
|
@ -187,22 +234,6 @@ func (app *Application) ConfigurationReadOnly() context.ConfigurationReadOnly {
|
||||||
return app.config
|
return app.config
|
||||||
}
|
}
|
||||||
|
|
||||||
// Maybe, if it's requested:
|
|
||||||
// func (app *Application) SetName(appName string) *iris.Application {
|
|
||||||
// app.config.name = appName
|
|
||||||
// app.logger.SetChildPrefix(appName)
|
|
||||||
// return app
|
|
||||||
// }
|
|
||||||
|
|
||||||
func newLogger(app *Application) *golog.Logger {
|
|
||||||
logger := golog.Default.Child(app)
|
|
||||||
if prefix := os.Getenv("IRIS_APP_NAME"); prefix != "" {
|
|
||||||
logger.SetChildPrefix(prefix)
|
|
||||||
}
|
|
||||||
|
|
||||||
return logger
|
|
||||||
}
|
|
||||||
|
|
||||||
// Logger returns the golog logger instance(pointer) that is being used inside the "app".
|
// Logger returns the golog logger instance(pointer) that is being used inside the "app".
|
||||||
//
|
//
|
||||||
// Available levels:
|
// Available levels:
|
||||||
|
@ -488,6 +519,12 @@ func (app *Application) Build() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if cb := app.OnBuild; cb != nil {
|
||||||
|
if err := cb(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// start := time.Now()
|
// start := time.Now()
|
||||||
app.builded = true // even if fails.
|
app.builded = true // even if fails.
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user