diff --git a/HISTORY.md b/HISTORY.md index e1fb2fbc..39a44971 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -551,7 +551,9 @@ New Context Methods: - `Context.CompressReader(enable bool)` method and `iris.CompressReader` middleware to enable future request read body calls to decompress data, [example](_examples/compression/main.go). - `Context.RegisterDependency(v interface{})` and `Context.UnregisterDependency(typ reflect.Type)` to register/remove struct dependencies on serve-time through a middleware. - `Context.SetID(id interface{})` and `Context.GetID() interface{}` added to register a custom unique indetifier to the Context, if necessary. -- `Context.GetDomain() string` returns the domain. +- `Context.Scheme() string` returns the full scheme of the request URL. +- `Context.SubdomainFull() string` returns the full subdomain(s) part of the host (`host[0:rootLevelDomain]`). +- `Context.Domain() string` returns the root level domain. - `Context.AddCookieOptions(...CookieOption)` adds options for `SetCookie`, `SetCookieKV, UpsertCookie` and `RemoveCookie` methods for the current request. - `Context.ClearCookieOptions()` clears any cookie options registered through `AddCookieOptions`. - `Context.SetLanguage(langCode string)` force-sets a language code from inside a middleare, similar to the `app.I18n.ExtractFunc` diff --git a/_examples/routing/subdomains/wildcard/hosts b/_examples/routing/subdomains/wildcard/hosts index 3b3b5664..a2cd47cf 100644 --- a/_examples/routing/subdomains/wildcard/hosts +++ b/_examples/routing/subdomains/wildcard/hosts @@ -26,5 +26,5 @@ 127.0.0.1 username3.mydomain.com 127.0.0.1 username4.mydomain.com 127.0.0.1 username5.mydomain.com - +127.0.0.1 en-us.test.mydomain.com #-END iris- diff --git a/_examples/routing/subdomains/wildcard/main.go b/_examples/routing/subdomains/wildcard/main.go index 2b78dfe8..0eb3dc30 100644 --- a/_examples/routing/subdomains/wildcard/main.go +++ b/_examples/routing/subdomains/wildcard/main.go @@ -54,6 +54,7 @@ func main() { // http://username1.mydomain.com:8080 // http://username2.mydomain.com:8080/something // http://username3.mydomain.com:8080/something/yourname + // http://en-us.test.mydomain.com:8080/something/42 app.Listen("mydomain.com:8080") // for beginners: look ../hosts file } @@ -66,6 +67,6 @@ func dynamicSubdomainHandler(ctx iris.Context) { func dynamicSubdomainHandlerWithParam(ctx iris.Context) { username := ctx.Subdomain() - ctx.Writef("Hello from dynamic subdomain path: %s, here you can handle the route for dynamic subdomains, handle the user: %s", ctx.Path(), username) + ctx.Writef("Hello from dynamic (full) subdomain: %s and path: %s, here you can handle the route for dynamic subdomains, handle the user: %s", ctx.SubdomainFull(), ctx.Path(), username) ctx.Writef("The paramfirst is: %s", ctx.Params().Get("paramfirst")) } diff --git a/context/context.go b/context/context.go index d2ec69ae..b8084379 100644 --- a/context/context.go +++ b/context/context.go @@ -810,6 +810,28 @@ func (ctx *Context) RequestPath(escape bool) string { return ctx.request.URL.Path // RawPath returns empty, requesturi can be used instead also. } +const sufscheme = "://" + +// GetScheme returns the full scheme of the request URL (https://, http:// or ws:// and e.t.c.``). +func GetScheme(r *http.Request) string { + scheme := r.URL.Scheme + + if scheme == "" { + if r.TLS != nil { + scheme = netutil.SchemeHTTPS + } else { + scheme = netutil.SchemeHTTP + } + } + + return scheme + sufscheme +} + +// Scheme returns the full scheme of the request (including :// suffix). +func (ctx *Context) Scheme() string { + return GetScheme(ctx.Request()) +} + // PathPrefixMap accepts a map of string and a handler. // The key of "m" is the key, which is the prefix, regular expressions are not valid. // The value of "m" is the handler that will be executed if HasPrefix(context.Path). @@ -824,6 +846,15 @@ func (ctx *Context) RequestPath(escape bool) string { // return false // } no, it will not work because map is a random peek data structure. +// GetHost returns the host part of the current URI. +func GetHost(r *http.Request) string { + // contains subdomain. + if host := r.URL.Host; host != "" { + return host + } + return r.Host +} + // Host returns the host:port part of the request URI, calls the `Request().Host`. // To get the subdomain part as well use the `Request().URL.Host` method instead. // To get the subdomain only use the `Subdomain` method instead. @@ -842,17 +873,47 @@ func (ctx *Context) Host() string { return GetHost(ctx.request) } -// GetHost returns the host part of the current URI. -func GetHost(r *http.Request) string { - // contains subdomain. - if host := r.URL.Host; host != "" { +// GetDomain resolves and returns the server's domain. +func GetDomain(hostport string) string { + host := hostport + if tmp, _, err := net.SplitHostPort(hostport); err == nil { + host = tmp + } + + switch host { + case "127.0.0.1", "0.0.0.0", "::1", "[::1]", "0:0:0:0:0:0:0:0", "0:0:0:0:0:0:0:1": + // loopback. + return "localhost" + default: + if domain, err := publicsuffix.EffectiveTLDPlusOne(host); err == nil { + host = domain + } + return host } - return r.Host } -// Subdomain returns the subdomain of this request, if any. -// Note that this is a fast method which does not cover all cases. +// Domain returns the root level domain. +func (ctx *Context) Domain() string { + return GetDomain(ctx.Host()) +} + +// SubdomainFull returnst he full subdomain level, e.g. +// [test.user.]mydomain.com. +func (ctx *Context) SubdomainFull() string { + host := ctx.Host() // host:port + rootDomain := GetDomain(host) // mydomain.com + rootDomainIdx := strings.Index(host, rootDomain) + if rootDomainIdx == -1 { + return "" + } + + return host[0:rootDomainIdx] +} + +// Subdomain returns the first subdomain of this request, +// e.g. [user.]mydomain.com. +// See `SubdomainFull` too. func (ctx *Context) Subdomain() (subdomain string) { host := ctx.Host() if index := strings.IndexByte(host, '.'); index > 0 { @@ -962,31 +1023,6 @@ func (ctx *Context) GetHeader(name string) string { return ctx.request.Header.Get(name) } -// GetDomain resolves and returns the server's domain. -func GetDomain(hostport string) string { - host := hostport - if tmp, _, err := net.SplitHostPort(hostport); err == nil { - host = tmp - } - - switch host { - case "127.0.0.1", "0.0.0.0", "::1", "[::1]", "0:0:0:0:0:0:0:0", "0:0:0:0:0:0:0:1": - // loopback. - return "localhost" - default: - if domain, err := publicsuffix.EffectiveTLDPlusOne(host); err == nil { - host = domain - } - - return host - } -} - -// GetDomain resolves and returns the server's domain. -func (ctx *Context) GetDomain() string { - return GetDomain(ctx.Host()) -} - // IsAjax returns true if this request is an 'ajax request'( XMLHttpRequest) // // There is no a 100% way of knowing that a request was made via Ajax. @@ -4072,7 +4108,7 @@ func CookieAllowSubdomains(cookieNames ...string) CookieOption { return } - c.Domain = ctx.GetDomain() + c.Domain = ctx.Domain() c.SameSite = http.SameSiteLaxMode // allow subdomain sharing. } } diff --git a/core/router/router_subdomain_redirect_wrapper.go b/core/router/router_subdomain_redirect.go similarity index 83% rename from core/router/router_subdomain_redirect_wrapper.go rename to core/router/router_subdomain_redirect.go index 17ac3697..2dbcdb53 100644 --- a/core/router/router_subdomain_redirect_wrapper.go +++ b/core/router/router_subdomain_redirect.go @@ -92,16 +92,6 @@ func NewSubdomainRedirectWrapper(rootDomainGetter func() string, from, to string return sd.Wrapper } -const sufscheme = "://" - -func getFullScheme(r *http.Request) string { - if !r.URL.IsAbs() { - // url scheme is empty. - return netutil.SchemeHTTP + sufscheme - } - return r.URL.Scheme + sufscheme -} - // Wrapper is the function that is being used to wrap the router with a redirect // service that is able to redirect between (sub)domains as fast as possible. // Please take a look at the `NewSubdomainRedirectWrapper` function for more. @@ -142,11 +132,11 @@ func (s *subdomainRedirectWrapper) Wrapper(w http.ResponseWriter, r *http.Reques resturi := r.URL.RequestURI() if s.isToRoot { // from a specific subdomain or any subdomain to the root domain. - redirectAbsolute(w, r, getFullScheme(r)+root+resturi, http.StatusMovedPermanently) + redirectAbsolute(w, r, context.GetScheme(r)+root+resturi, http.StatusMovedPermanently) return } // from a specific subdomain or any subdomain to a specific subdomain. - redirectAbsolute(w, r, getFullScheme(r)+s.to+root+resturi, http.StatusMovedPermanently) + redirectAbsolute(w, r, context.GetScheme(r)+s.to+root+resturi, http.StatusMovedPermanently) return } @@ -172,7 +162,7 @@ func (s *subdomainRedirectWrapper) Wrapper(w http.ResponseWriter, r *http.Reques resturi := r.URL.RequestURI() // we are not inside a subdomain, so we are in the root domain // and the redirect is configured to be used from root domain to a subdomain. - redirectAbsolute(w, r, getFullScheme(r)+s.to+root+resturi, http.StatusMovedPermanently) + redirectAbsolute(w, r, context.GetScheme(r)+s.to+root+resturi, http.StatusMovedPermanently) return } @@ -199,3 +189,30 @@ func redirectAbsolute(w http.ResponseWriter, r *http.Request, url string, code i 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. +func NewSubdomainPartyRedirectHandler(to Party) context.Handler { + return NewSubdomainRedirectHandler(to.GetRelPath()) +} + +// NewSubdomainRedirectHandler returns a handler which can be registered +// through `UseRouter` or `Use` to redirect from the current request's +// subdomain to the given "toSubdomain". +func NewSubdomainRedirectHandler(toSubdomain string) context.Handler { + toSubdomain, _ = splitSubdomainAndPath(toSubdomain) // let it here so users can just pass the GetRelPath of a Party. + if pathIsWildcard(toSubdomain) { + return nil + } + + return func(ctx *context.Context) { + // 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) + } +} diff --git a/core/router/router_test.go b/core/router/router_test.go index 17f109de..73008385 100644 --- a/core/router/router_test.go +++ b/core/router/router_test.go @@ -159,3 +159,40 @@ func TestRouterWrapperOrder(t *testing.T) { e.GET("/").Expect().Status(iris.StatusOK).Body().Equal(expectedOrderStr) } } + +func TestNewSubdomainPartyRedirectHandler(t *testing.T) { + app := iris.New() + app.Get("/", func(ctx iris.Context) { + ctx.WriteString("root index") + }) + + test := app.Subdomain("test") + test.OnErrorCode(iris.StatusNotFound, func(ctx iris.Context) { + ctx.WriteString("test 404") + }) + test.Get("/", func(ctx iris.Context) { + ctx.WriteString("test index") + }) + + testold := app.Subdomain("testold") + // redirects testold.mydomain.com to test.mydomain.com . + testold.UseRouter(router.NewSubdomainPartyRedirectHandler(test)) + testold.Get("/", func(ctx iris.Context) { + ctx.WriteString("test old index (should never be fired)") + }) + testoldLeveled := testold.Subdomain("leveled") + testoldLeveled.Get("/", func(ctx iris.Context) { + ctx.WriteString("leveled.testold this can be fired") + }) + + if redirectHandler := router.NewSubdomainPartyRedirectHandler(app.WildcardSubdomain()); redirectHandler != nil { + t.Fatal("redirect handler should be nil, we cannot redirect to a wildcard") + } + + e := httptest.New(t, app) + e.GET("/").WithURL("http://mydomain.com").Expect().Status(iris.StatusOK).Body().Equal("root index") + e.GET("/").WithURL("http://test.mydomain.com").Expect().Status(iris.StatusOK).Body().Equal("test index") + e.GET("/").WithURL("http://testold.mydomain.com").Expect().Status(iris.StatusOK).Body().Equal("test index") + e.GET("/").WithURL("http://testold.mydomain.com/notfound").Expect().Status(iris.StatusNotFound).Body().Equal("test 404") + e.GET("/").WithURL("http://leveled.testold.mydomain.com").Expect().Status(iris.StatusOK).Body().Equal("leveled.testold this can be fired") +}