diff --git a/HISTORY_GR.md b/HISTORY_GR.md index a4beb019..fcad3d94 100644 --- a/HISTORY_GR.md +++ b/HISTORY_GR.md @@ -21,7 +21,7 @@ ## Ασφάλεια | `iris.AutoTLS` -**Όλοι οι servers πρέπει να αναβαθμιστούν σε αυτήν την έκδοση**, περιέχει διορθώσεις για το _tls-sni challenge_ το οποίο απενεργοποιήθηκε μερικές μέρες πριν από το letsencrypt.org το οποίο προκάλεσε σχεδόν όλα τα golang https-ενεργποιημένα servers να να μην είναι σε θέση να λειτουργήσουν, έτσι υποστήριξη για το _http-01 challenge_ προστέθηκε σαν αναπλήρωση. Πλέον ο διακομιστής δοκιμάζει όλες τις διαθέσιμες προκλήσεις(challeneges) letsencrypt. +**Όλοι οι servers πρέπει να αναβαθμιστούν σε αυτήν την έκδοση**, περιέχει διορθώσεις για το _tls-sni challenge_ το οποίο απενεργοποιήθηκε μερικές μέρες πριν από το letsencrypt.org το οποίο προκάλεσε σχεδόν όλα τα golang https-ενεργποιημένα servers να να μην είναι σε θέση να λειτουργήσουν, έτσι υποστήριξη για το _http-01 challenge_ προστέθηκε σαν αναπλήρωση. Πλέον ο διακομιστής δοκιμάζει όλες τις διαθέσιμες προκλήσεις(challenges) letsencrypt. Διαβάστε περισσότερα: diff --git a/README_RU.md b/README_RU.md index 85bcb688..85715d81 100644 --- a/README_RU.md +++ b/README_RU.md @@ -1,4 +1,4 @@ -# Веб-фреймворк Iris +# Iris Web Framework diff --git a/_examples/README.md b/_examples/README.md index f5a1e0e5..b80ccda7 100644 --- a/_examples/README.md +++ b/_examples/README.md @@ -10,6 +10,7 @@ It doesn't always contain the "best ways" but it does cover each important featu - [Hello world!](hello-world/main.go) - [Glimpse](overview/main.go) +- [WWW](www/main.go) - [Tutorial: Online Visitors](tutorial/online-visitors/main.go) - [Tutorial: A Todo MVC Application using Iris and Vue.js](https://hackernoon.com/a-todo-mvc-application-using-iris-and-vue-js-5019ff870064) - [Tutorial: URL Shortener using BoltDB](https://medium.com/@kataras/a-url-shortener-service-using-go-iris-and-bolt-4182f0b00ae7) @@ -267,7 +268,8 @@ Follow the examples below, - [Single](subdomains/single/main.go) - [Multi](subdomains/multi/main.go) - [Wildcard](subdomains/wildcard/main.go) -- [WWW](subdomains/www/main.go) +- [WWW](subdomains/www/main.go) +- [Redirect fast](subdomains/redirect/main.go) ### Convert `http.Handler/HandlerFunc` diff --git a/_examples/subdomains/redirect/hosts b/_examples/subdomains/redirect/hosts new file mode 100644 index 00000000..7437d713 --- /dev/null +++ b/_examples/subdomains/redirect/hosts @@ -0,0 +1,4 @@ +127.0.0.1 mydomain.com +127.0.0.1 www.mydomain.com + +# Windows: Drive:/Windows/system32/drivers/etc/hosts, on Linux: /etc/hosts \ No newline at end of file diff --git a/_examples/subdomains/redirect/main.go b/_examples/subdomains/redirect/main.go new file mode 100644 index 00000000..27e478fe --- /dev/null +++ b/_examples/subdomains/redirect/main.go @@ -0,0 +1,68 @@ +// Package main shows how to register a simple 'www' subdomain, +// using the `app.WWW` method, which will register a router wrapper which will +// redirect all 'mydomain.com' requests to 'www.mydomain.com'. +// Check the 'hosts' file to see how to test the 'mydomain.com' on your local machine. +package main + +import "github.com/kataras/iris" + +const addr = "mydomain.com:80" + +func main() { + app := newApp() + + // http(s)://mydomain.com, will be redirect to http(s)://www.mydomain.com. + // The `www` variable is the `app.Subdomain("www")`. + // + // app.WWW() wraps the router so it can redirect all incoming requests + // that comes from 'http(s)://mydomain.com/%path%' (www is missing) + // to `http(s)://www.mydomain.com/%path%`. + // + // Try: + // http://mydomain.com -> http://www.mydomain.com + // http://mydomain.com/users -> http://www.mydomain.com/users + // http://mydomain.com/users/login -> http://www.mydomain.com/users/login + app.Run(iris.Addr(addr)) +} + +func newApp() *iris.Application { + app := iris.New() + app.Get("/", func(ctx iris.Context) { + ctx.Writef("This will never be executed.") + }) + + www := app.Subdomain("www") // <- same as app.Party("www.") + www.Get("/", index) + + // www is an `iris.Party`, use it like you already know, like grouping routes. + www.PartyFunc("/users", func(p iris.Party) { // <- same as www.Party("/users").Get(...) + p.Get("/", usersIndex) + p.Get("/login", getLogin) + }) + + // redirects mydomain.com/%anypath% to www.mydomain.com/%anypath%. + // First argument is the 'from' and second is the 'to/target'. + app.SubdomainRedirect(app, www) + + // If you need to redirect any subdomain to 'www' then: + // app.SubdomainRedirect(app.WildcardSubdomain(), www) + // If you need to redirect from a subdomain to the root domain then: + // app.SubdomainRedirect(app.Subdomain("mysubdomain"), app) + // + // Note that app.Party("mysubdomain.") and app.Subdomain("mysubdomain") + // is the same exactly thing, the difference is that the second can omit the last dot('.'). + + return app +} + +func index(ctx iris.Context) { + ctx.Writef("This is the www.mydomain.com endpoint.") +} + +func usersIndex(ctx iris.Context) { + ctx.Writef("This is the www.mydomain.com/users endpoint.") +} + +func getLogin(ctx iris.Context) { + ctx.Writef("This is the www.mydomain.com/users/login endpoint.") +} diff --git a/_examples/subdomains/redirect/main_test.go b/_examples/subdomains/redirect/main_test.go new file mode 100644 index 00000000..cc4f931d --- /dev/null +++ b/_examples/subdomains/redirect/main_test.go @@ -0,0 +1,33 @@ +package main + +import ( + "fmt" + "strings" + "testing" + + "github.com/kataras/iris/httptest" +) + +func TestSubdomainRedirectWWW(t *testing.T) { + app := newApp() + root := strings.TrimSuffix(addr, ":80") + + e := httptest.New(t, app) + + tests := []struct { + path string + response string + }{ + {"/", fmt.Sprintf("This is the www.%s endpoint.", root)}, + {"/users", fmt.Sprintf("This is the www.%s/users endpoint.", root)}, + {"/users/login", fmt.Sprintf("This is the www.%s/users/login endpoint.", root)}, + } + + for _, test := range tests { + req := e.GET(test.path) + // req.WithURL("http://www." + root) + + req.Expect().Status(httptest.StatusOK).Body().Equal(test.response) + } + +} diff --git a/_examples/subdomains/www/main.go b/_examples/subdomains/www/main.go index b80a28e3..26dc2672 100644 --- a/_examples/subdomains/www/main.go +++ b/_examples/subdomains/www/main.go @@ -11,25 +11,41 @@ func newApp() *iris.Application { app.Get("/about", info) app.Get("/contact", info) - usersAPI := app.Party("/api/users") - { - usersAPI.Get("/", info) - usersAPI.Get("/{id:int}", info) + app.PartyFunc("/api/users", func(r iris.Party) { + r.Get("/", info) + r.Get("/{id:int}", info) - usersAPI.Post("/", info) + r.Post("/", info) - usersAPI.Put("/{id:int}", info) - } + r.Put("/{id:int}", info) + }) /* <- same as: + usersAPI := app.Party("/api/users") + { // those brackets are just syntactic-sugar things. + // This method is rarely used but you can make use of it when you want + // scoped variables to that code block only. + usersAPI.Get/Post... + } + usersAPI.Get/Post... + */ www := app.Party("www.") { - // get all routes that are registered so far, including all "Parties" but subdomains: + // http://www.mydomain.com/hi + www.Get("/hi", func(ctx iris.Context) { + ctx.Writef("hi from www.mydomain.com") + }) + + // Just to show how you can get all routes and copy them to another + // party or subdomain: + // Get all routes that are registered so far, including all "Parties" but subdomains: currentRoutes := app.GetRoutes() - // register them to the www subdomain/vhost as well: + // Register them to the www subdomain/vhost as well: for _, r := range currentRoutes { www.Handle(r.Method, r.Path, r.Handlers...) } } + // See also the "subdomains/redirect" to register redirect router wrappers between subdomains, + // i.e mydomain.com to www.mydomain.com (like facebook does for SEO reasons(;)). return app } @@ -43,6 +59,7 @@ func main() { // http://mydomain.com/api/users/42 // http://www.mydomain.com + // http://www.mydomain.com/hi // http://www.mydomain.com/about // http://www.mydomain.com/contact // http://www.mydomain.com/api/users diff --git a/configuration.go b/configuration.go index 02526963..4fb3f0ec 100644 --- a/configuration.go +++ b/configuration.go @@ -735,7 +735,7 @@ func WithConfiguration(c Configuration) Configurator { if v := c.RemoteAddrHeaders; len(v) > 0 { if main.RemoteAddrHeaders == nil { - main.RemoteAddrHeaders = make(map[string]bool) + main.RemoteAddrHeaders = make(map[string]bool, len(v)) } for key, value := range v { main.RemoteAddrHeaders[key] = value @@ -744,7 +744,7 @@ func WithConfiguration(c Configuration) Configurator { if v := c.Other; len(v) > 0 { if main.Other == nil { - main.Other = make(map[string]interface{}) + main.Other = make(map[string]interface{}, len(v)) } for key, value := range v { main.Other[key] = value diff --git a/context/context.go b/context/context.go index 2422ad8d..a776143b 100644 --- a/context/context.go +++ b/context/context.go @@ -366,6 +366,8 @@ type Context interface { // Subdomain returns the subdomain of this request, if any. // Note that this is a fast method which does not cover all cases. Subdomain() (subdomain string) + // IsWWW returns true if the current subdomain (if any) is www. + IsWWW() bool // RemoteAddr tries to parse and return the real client's request IP. // // Based on allowed headers names that can be modified from Configuration.RemoteAddrHeaders. @@ -1311,11 +1313,16 @@ func (ctx *context) RequestPath(escape bool) string { // return false // } no, it will not work because map is a random peek data structure. -// Host returns the host part of the current url. +// Host returns the host part of the current URI. func (ctx *context) Host() string { - h := ctx.request.URL.Host + return GetHost(ctx.request) +} + +// GetHost returns the host part of the current URI. +func GetHost(r *http.Request) string { + h := r.URL.Host if h == "" { - h = ctx.request.Host + h = r.Host } return h } @@ -1338,6 +1345,18 @@ func (ctx *context) Subdomain() (subdomain string) { return } +// IsWWW returns true if the current subdomain (if any) is www. +func (ctx *context) IsWWW() bool { + host := ctx.Host() + if index := strings.IndexByte(host, '.'); index > 0 { + // if it has a subdomain and it's www then return true. + if subdomain := host[0:index]; !strings.Contains(ctx.Application().ConfigurationReadOnly().GetVHost(), subdomain) { + return subdomain == "www" + } + } + return false +} + // RemoteAddr tries to parse and return the real client's request IP. // // Based on allowed headers names that can be modified from Configuration.RemoteAddrHeaders. diff --git a/core/netutil/addr.go b/core/netutil/addr.go index 66ee0aef..d1d5f36c 100644 --- a/core/netutil/addr.go +++ b/core/netutil/addr.go @@ -195,8 +195,8 @@ func ResolvePort(addr string) int { return 80 } -// ResolveScheme returns "https://" if "isTLS" receiver is true, -// otherwise "http://". +// ResolveScheme returns "https" if "isTLS" receiver is true, +// otherwise "http". func ResolveScheme(isTLS bool) string { if isTLS { return SchemeHTTPS diff --git a/core/router/api_builder.go b/core/router/api_builder.go index ebc278c1..4fb3bb46 100644 --- a/core/router/api_builder.go +++ b/core/router/api_builder.go @@ -111,6 +111,14 @@ func NewAPIBuilder() *APIBuilder { return api } +// GetRelPath returns the current party's relative path. +// i.e: +// if r := app.Party("/users"), then the `r.GetRelPath()` is the "/users". +// if r := app.Party("www.") or app.Subdomain("www") then the `r.GetRelPath()` is the "www.". +func (api *APIBuilder) GetRelPath() string { + return api.relativePath +} + // GetReport returns an error may caused by party's methods. func (api *APIBuilder) GetReport() error { return api.reporter.Return() @@ -292,7 +300,7 @@ func (api *APIBuilder) PartyFunc(relativePath string, partyBuilderFunc func(p Pa // this specific "subdomain". // // If called from a child party then the subdomain will be prepended to the path instead of appended. -// So if app.Subdomain("admin.").Subdomain("panel.") then the result is: "panel.admin.". +// So if app.Subdomain("admin").Subdomain("panel") then the result is: "panel.admin.". func (api *APIBuilder) Subdomain(subdomain string, middleware ...context.Handler) Party { if api.relativePath == SubdomainWildcardIndicator { // cannot concat wildcard subdomain with something else @@ -300,6 +308,12 @@ func (api *APIBuilder) Subdomain(subdomain string, middleware ...context.Handler api.relativePath, subdomain) return api } + if l := len(subdomain); l < 1 { + return api + } else if subdomain[l-1] != '.' { + subdomain += "." + } + return api.Party(subdomain, middleware...) } diff --git a/core/router/party.go b/core/router/party.go index f1a1a9d0..9f840539 100644 --- a/core/router/party.go +++ b/core/router/party.go @@ -14,6 +14,11 @@ import ( // // Look the "APIBuilder" for its implementation. type Party interface { + // GetRelPath returns the current party's relative path. + // i.e: + // if r := app.Party("/users"), then the `r.GetRelPath()` is the "/users". + // if r := app.Party("www.") or app.Subdomain("www") then the `r.GetRelPath()` is the "www.". + GetRelPath() string // GetReporter returns the reporter for adding errors GetReporter() *errors.Reporter // Macros returns the macro map which is responsible diff --git a/core/router/router.go b/core/router/router.go index 805c0df1..9b81d2ea 100644 --- a/core/router/router.go +++ b/core/router/router.go @@ -114,13 +114,13 @@ type WrapperFunc func(w http.ResponseWriter, r *http.Request, firstNextIsTheRout // // Before build. func (router *Router) WrapRouter(wrapperFunc WrapperFunc) { - router.mu.Lock() - defer router.mu.Unlock() - if wrapperFunc == nil { return } + router.mu.Lock() + defer router.mu.Unlock() + if router.wrapperFunc != nil { // wrap into one function, from bottom to top, end to begin. nextWrapper := wrapperFunc diff --git a/core/router/router_subdomain_redirect_wrapper.go b/core/router/router_subdomain_redirect_wrapper.go new file mode 100644 index 00000000..beb438f6 --- /dev/null +++ b/core/router/router_subdomain_redirect_wrapper.go @@ -0,0 +1,163 @@ +package router + +import ( + "net/http" + "strings" + + "github.com/kataras/iris/context" + "github.com/kataras/iris/core/netutil" +) + +type subdomainRedirectWrapper struct { + // the func which will give us the root domain, + // it's declared as a func because in that state the application is not configurated neither ran yet. + root func() string + // the from and to locations, if subdomains must end with dot('.'). + from, to string + // true if from wildcard subdomain is given by 'from' ("*." or '*'). + isFromAny bool + // true for the location that is the root domain ('/', '.' or ""). + isFromRoot, isToRoot bool +} + +func pathIsRootDomain(partyRelPath string) bool { + return partyRelPath == "/" || partyRelPath == "" || partyRelPath == "." +} + +func pathIsWildcard(partyRelPath string) bool { + return partyRelPath == SubdomainWildcardIndicator || partyRelPath == "*" +} + +// NewSubdomainRedirectWrapper returns a router wrapper which +// if it's registered to the router via `router#WrapRouter` it +// redirects(StatusMovedPermanently) a subdomain or the root domain to another subdomain or to the root domain. +// +// It receives three arguments, +// the first one is a function which returns the root domain, (in the application it's the app.ConfigurationReadOnly().GetVHost()). +// The second and third are the from and to locations, 'from' can be a wildcard subdomain as well (*. or *) +// 'to' is not allowed to be a wildcard for obvious reasons, +// 'from' can be the root domain when the 'to' is not the root domain and visa-versa. +// To declare a root domain as 'from' or 'to' you MUST pass an empty string or a slash('/') or a dot('.'). +// Important note: the 'from' and 'to' should end with "." like we use the `APIBuilder#Party`, if they are subdomains. +// +// Usage(package-level): +// sd := NewSubdomainRedirectWrapper(func() string { return "mydomain.com" }, ".", "www.") +// router.WrapRouter(sd) +// +// Usage(high-level using `iris#Application.SubdomainRedirect`) +// www := app.Subdomain("www") +// app.SubdomainRedirect(app, www) +// Because app's rel path is "/" it translates it to the root domain +// and www's party's rel path is the "www.", so it's the target subdomain. +// +// All the above code snippets will register a router wrapper which will +// redirect all http(s)://mydomain.com/%anypath% to http(s)://www.mydomain.com/%anypath%. +// +// One or more subdomain redirect wrappers can be used to the same router instance. +// +// NewSubdomainRedirectWrapper may return nil if not allowed input arguments values were received +// but in that case, the `WrapRouter` will, simply, ignore that wrapper. +// +// Example: https://github.com/kataras/iris/tree/master/_examples/subdomains/redirect +func NewSubdomainRedirectWrapper(rootDomainGetter func() string, from, to string) WrapperFunc { + // we can return nil, + // because if wrapper is nil then it's not be used on the `router#WrapRouter`. + if from == to { + // cannot redirect to the same location, cycle. + return nil + } + + if pathIsWildcard(to) { + // cannot redirect to "any location". + return nil + } + + isFromRoot, isToRoot := pathIsRootDomain(from), pathIsRootDomain(to) + if isFromRoot && isToRoot { + // cannot redirect to the root domain from the root domain. + return nil + } + + sd := &subdomainRedirectWrapper{ + root: rootDomainGetter, + from: from, + to: to, + isFromAny: pathIsWildcard(from), + isFromRoot: isFromRoot, + isToRoot: isToRoot, + } + + 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. +func (s *subdomainRedirectWrapper) Wrapper(w http.ResponseWriter, r *http.Request, router http.HandlerFunc) { + // Author's note: + // I use the StatusMovedPermanently(301) instead of the the StatusPermanentRedirect(308) + // because older browsers may not be able to recognise that status code (the RFC 7538, is not so old) + // although note that move is not the same thing as redirect: move reminds a specific address or location moved while + // redirect is a new location. + + host := context.GetHost(r) + root := s.root() + hasSubdomain := host != root + + if !hasSubdomain && !s.isFromRoot { + // if the current endpoint is not a subdomain + // and the redirect is not configured to be used from root domain to a subdomain. + // This check comes first because it's the most common scenario. + router(w, r) + return + } + + if hasSubdomain { + // the current endpoint is a subdomain and + // redirect is used for a subdomain to another subdomain or to its root domain. + subdomain := strings.TrimSuffix(host, root) // with dot '.'. + if s.to == subdomain { + // we are in the subdomain we wanted to be redirected, + // remember: a redirect response will fire a new request. + // This check is needed to not allow cycles (too many redirects). + router(w, r) + return + } + + if subdomain == s.from || s.isFromAny { + resturi := r.URL.RequestURI() + if s.isToRoot { + // from a specific subdomain or any subdomain to the root domain. + http.Redirect(w, r, getFullScheme(r)+root+resturi, http.StatusMovedPermanently) + return + } + // from a specific subdomain or any subdomain to a specific subdomain. + http.Redirect(w, r, getFullScheme(r)+s.to+root+resturi, http.StatusMovedPermanently) + return + } + + // the from subdomain is not matched and it's not from root. + router(w, r) + return + } + + if s.isFromRoot { + 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. + http.Redirect(w, r, getFullScheme(r)+s.to+root+resturi, http.StatusMovedPermanently) + return + } + + router(w, r) +} diff --git a/iris.go b/iris.go index 6c6205bd..846a2c29 100644 --- a/iris.go +++ b/iris.go @@ -187,6 +187,41 @@ func Default() *Application { return app } +// 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. +// Other subdomains can be registered using the app: `sub := app.Subdomain("mysubdomain")`, +// child subdomains can be registered using the www := app.WWW(); www.Subdomain("wwwchildSubdomain"). +func (app *Application) WWW() router.Party { + return app.SubdomainRedirect(app, app.Subdomain("www")) +} + +// SubdomainRedirect registers a router wrapper which +// redirects(StatusMovedPermanently) a (sub)domain to another subdomain or to the root domain as fast as possible, +// before the router's try to execute route's handler(s). +// +// It receives two arguments, they are the from and to/target locations, +// 'from' can be a wildcard subdomain as well (app.WildcardSubdomain()) +// 'to' is not allowed to be a wildcard for obvious reasons, +// 'from' can be the root domain(app) when the 'to' is not the root domain and visa-versa. +// +// Usage: +// www := app.Subdomain("www") <- same as app.Party("www.") +// app.SubdomainRedirect(app, www) +// This will redirect all http(s)://mydomain.com/%anypath% to http(s)://www.mydomain.com/%anypath%. +// +// One or more subdomain redirects can be used to the same app instance. +// +// If you need more information about this implementation then you have to navigate through +// the `core/router#NewSubdomainRedirectWrapper` function instead. +// +// Example: https://github.com/kataras/iris/tree/master/_examples/subdomains/redirect +func (app *Application) SubdomainRedirect(from, to router.Party) router.Party { + sd := router.NewSubdomainRedirectWrapper(app.ConfigurationReadOnly().GetVHost, from.GetRelPath(), to.GetRelPath()) + app.WrapRouter(sd) + return to +} + // Configure can called when modifications to the framework instance needed. // It accepts the framework instance // and returns an error which if it's not nil it's printed to the logger. @@ -670,7 +705,7 @@ var ErrServerClosed = http.ErrServerClosed // `Listener`, `Server`, `Addr`, `TLS`, `AutoTLS` and `Raw`. func (app *Application) Run(serve Runner, withOrWithout ...Configurator) error { // first Build because it doesn't need anything from configuration, - // this give the user the chance to modify the router inside a configurator as well. + // this give the user the chance to modify the router inside a configurator as well. if err := app.Build(); err != nil { return errors.PrintAndReturnErrors(err, app.logger.Errorf) }