diff --git a/.gitignore b/.gitignore
index 462f0d0e..f4787c15 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,6 @@
.vscode
.directory
+coverage.out
package-lock.json
go.sum
node_modules
diff --git a/.travis.yml b/.travis.yml
index e87b8cd3..89e6e3f6 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -5,6 +5,8 @@ os:
- linux
- osx
go:
+# We support the latest two major Go versions:
+# https://golang.org/doc/devel/release.html
- 1.14.x
- 1.15.x
# - master
@@ -12,6 +14,15 @@ go_import_path: github.com/kataras/iris/v12
env:
global:
- GO111MODULE=on
+addons:
+ hosts:
+ - mydomain.com
+ - www.mydomain.com
+ - myotherdomain.com
+ - mymy.com
+ - testdomain.com
+ - testdomain1.com
+ - testdomain2.com
install:
- go get ./...
script:
diff --git a/apps/apps.go b/apps/apps.go
new file mode 100644
index 00000000..dd492c5f
--- /dev/null
+++ b/apps/apps.go
@@ -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
diff --git a/apps/switch.go b/apps/switch.go
new file mode 100644
index 00000000..5d7aa233
--- /dev/null
+++ b/apps/switch.go
@@ -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
+}
diff --git a/apps/switch_go_test.go b/apps/switch_go_test.go
new file mode 100644
index 00000000..71780bd3
--- /dev/null
+++ b/apps/switch_go_test.go
@@ -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)
+ }
+
+}
diff --git a/apps/switch_hosts.go b/apps/switch_hosts.go
new file mode 100644
index 00000000..f143d4f7
--- /dev/null
+++ b/apps/switch_hosts.go
@@ -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
+}
diff --git a/apps/switch_hosts_test.go b/apps/switch_hosts_test.go
new file mode 100644
index 00000000..da9cc1f7
--- /dev/null
+++ b/apps/switch_hosts_test.go
@@ -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)
+ }
+ }
+ }
+}
diff --git a/apps/switch_scheme.go b/apps/switch_scheme.go
new file mode 100644
index 00000000..cff2ab90
--- /dev/null
+++ b/apps/switch_scheme.go
@@ -0,0 +1 @@
+package apps
diff --git a/context/application.go b/context/application.go
index ee0fbe4f..e2f52874 100644
--- a/context/application.go
+++ b/context/application.go
@@ -84,9 +84,30 @@ type Application interface {
//
// Order may change.
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 (
+ // 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
mu sync.RWMutex
)
@@ -107,8 +128,8 @@ func RegisterApplication(app Application) {
// use `Context.Application()` instead.
func LastApplication() Application {
mu.RLock()
- if n := len(registeredApps); n > 0 {
- if app := registeredApps[n-1]; app != nil {
+ for i := len(registeredApps) - 1; i >= 0; i-- {
+ if app := registeredApps[i]; app != nil {
mu.RUnlock()
return app
}
@@ -117,6 +138,34 @@ func LastApplication() Application {
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.
// If the program contains at least one registered Iris Application
// before this call then it will return a child of that Application's Logger
diff --git a/context/context.go b/context/context.go
index b8084379..b2d9de41 100644
--- a/context/context.go
+++ b/context/context.go
@@ -135,6 +135,14 @@ func NewContext(app Application) *Context {
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
// can be safely used outside the request's scope.
// Note that if the request-response lifecycle terminated
@@ -898,8 +906,22 @@ func (ctx *Context) Domain() string {
return GetDomain(ctx.Host())
}
-// SubdomainFull returnst he full subdomain level, e.g.
+// GetSubdomainFull returns the full subdomain level, e.g.
// [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 {
host := ctx.Host() // host:port
rootDomain := GetDomain(host) // mydomain.com
diff --git a/context/handler.go b/context/handler.go
index 6e653536..f4d7056c 100644
--- a/context/handler.go
+++ b/context/handler.go
@@ -19,7 +19,7 @@ var (
)
var (
- handlerNames = make(map[*nameExpr]string)
+ handlerNames = make(map[*NameExpr]string)
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,
// e.g. `iris/cache/client.(*Handler).ServeHTTP-fm`
regex, _ := regexp.Compile(original)
- handlerNames[&nameExpr{
+ handlerNames[&NameExpr{
literal: original,
regex: regex,
}] = replacement
handlerNamesMu.Unlock()
}
-type nameExpr struct {
+// NameExpr regex or literal comparison through `MatchString`.
+type NameExpr struct {
regex *regexp.Regexp
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.
return true
}
diff --git a/core/host/supervisor.go b/core/host/supervisor.go
index 3dc044b5..546f35d2 100644
--- a/core/host/supervisor.go
+++ b/core/host/supervisor.go
@@ -492,6 +492,9 @@ func (su *Supervisor) RegisterOnShutdown(cb func()) {
// for them to close, if desired.
func (su *Supervisor) Shutdown(ctx context.Context) error {
atomic.StoreUint32(&su.closedManually, 1) // future-use
+ if ctx == nil {
+ ctx = context.Background()
+ }
return su.Server.Shutdown(ctx)
}
diff --git a/core/router/router_subdomain_redirect.go b/core/router/router_subdomain_redirect.go
index 2dbcdb53..ed3fe86e 100644
--- a/core/router/router_subdomain_redirect.go
+++ b/core/router/router_subdomain_redirect.go
@@ -169,27 +169,6 @@ func (s *subdomainRedirectWrapper) Wrapper(w http.ResponseWriter, r *http.Reques
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 := "" + http.StatusText(code) + ".\n"
- fmt.Fprintln(w, body)
- }
-}
-
// NewSubdomainPartyRedirectHandler returns a handler which can be registered
// through `UseRouter` or `Use` to redirect from the current request's
// 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
host := ctx.Host()
fullSubdomain := ctx.SubdomainFull()
- newHost := strings.Replace(host, fullSubdomain, toSubdomain, 1)
- resturi := ctx.Request().URL.RequestURI()
- urlToRedirect := ctx.Scheme() + newHost + resturi
- redirectAbsolute(ctx.ResponseWriter(), ctx.Request(), urlToRedirect, http.StatusMovedPermanently)
+ targetHost := strings.Replace(host, fullSubdomain, toSubdomain, 1)
+ // resturi := ctx.Request().URL.RequestURI()
+ // urlToRedirect := ctx.Scheme() + newHost + resturi
+ 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 := "" + http.StatusText(code) + ".\n"
+ fmt.Fprintln(w, body)
}
}
diff --git a/iris.go b/iris.go
index f86555d5..b99efb95 100644
--- a/iris.go
+++ b/iris.go
@@ -82,8 +82,20 @@ type Application struct {
// used for build
builded 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
+ // 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 may be empty only if application ran(`app.Run`) with `iris.Raw` option runner,
@@ -131,6 +143,41 @@ func Default() *Application {
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.
// 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.
@@ -187,22 +234,6 @@ func (app *Application) ConfigurationReadOnly() context.ConfigurationReadOnly {
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".
//
// Available levels:
@@ -488,6 +519,12 @@ func (app *Application) Build() error {
return nil
}
+ if cb := app.OnBuild; cb != nil {
+ if err := cb(); err != nil {
+ return err
+ }
+ }
+
// start := time.Now()
app.builded = true // even if fails.