From fa1b3cfc0b0d74ffc11d594708bf06e32ed49a63 Mon Sep 17 00:00:00 2001 From: Nikita Tokarchuk Date: Tue, 16 Jan 2018 12:27:47 +0100 Subject: [PATCH 01/17] Fix dep dependency Former-commit-id: 0f1695ceb30c114d1487bc712a36b577d7d3c062 --- Gopkg.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gopkg.toml b/Gopkg.toml index 27f27ed5..e5d08baf 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -63,8 +63,8 @@ name = "github.com/ryanuber/columnize" [[constraint]] - branch = "v1.2.0" name = "github.com/satori/go.uuid" + version = "1.2.0" [[constraint]] branch = "master" From e176ff7b0c7177344e3572514fcf7325bd138e49 Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Sat, 20 Jan 2018 05:17:31 +0200 Subject: [PATCH 02/17] NEW: `Application#SubdomainRedirect`. Example: https://github.com/kataras/iris/blob/master/_examples/subdomains/redirect/main.go Former-commit-id: d8dd7c426dc9f14c870f103fef703595a2915612 --- HISTORY_GR.md | 2 +- README_RU.md | 2 +- _examples/README.md | 4 +- _examples/subdomains/redirect/hosts | 4 + _examples/subdomains/redirect/main.go | 68 ++++++++ _examples/subdomains/redirect/main_test.go | 33 ++++ _examples/subdomains/www/main.go | 35 +++- configuration.go | 4 +- context/context.go | 25 ++- core/netutil/addr.go | 4 +- core/router/api_builder.go | 16 +- core/router/party.go | 5 + core/router/router.go | 6 +- .../router_subdomain_redirect_wrapper.go | 163 ++++++++++++++++++ iris.go | 37 +++- 15 files changed, 384 insertions(+), 24 deletions(-) create mode 100644 _examples/subdomains/redirect/hosts create mode 100644 _examples/subdomains/redirect/main.go create mode 100644 _examples/subdomains/redirect/main_test.go create mode 100644 core/router/router_subdomain_redirect_wrapper.go 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) } From 25918427bc1862af39975a44044712f70265b4ab Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Sat, 20 Jan 2018 14:54:04 +0200 Subject: [PATCH 03/17] support multi-level subdomains redirect, see previous commit for the SubdomainRedirect fast solution Former-commit-id: bc5749e46d1ae65f9d17063f3d8f2ea72510a9d8 --- _examples/README.md | 1 - _examples/subdomains/redirect/hosts | 4 ++-- _examples/subdomains/redirect/main.go | 5 +++++ core/router/path.go | 5 ++++- iris.go | 2 +- 5 files changed, 12 insertions(+), 5 deletions(-) diff --git a/_examples/README.md b/_examples/README.md index b80ccda7..87fbf890 100644 --- a/_examples/README.md +++ b/_examples/README.md @@ -10,7 +10,6 @@ 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) diff --git a/_examples/subdomains/redirect/hosts b/_examples/subdomains/redirect/hosts index 7437d713..c4c06478 100644 --- a/_examples/subdomains/redirect/hosts +++ b/_examples/subdomains/redirect/hosts @@ -1,4 +1,4 @@ -127.0.0.1 mydomain.com -127.0.0.1 www.mydomain.com +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 index 27e478fe..87ec7a66 100644 --- a/_examples/subdomains/redirect/main.go +++ b/_examples/subdomains/redirect/main.go @@ -44,6 +44,11 @@ func newApp() *iris.Application { // First argument is the 'from' and second is the 'to/target'. app.SubdomainRedirect(app, www) + // SubdomainRedirect works for multi-level subdomains as well: + // subsub := www.Subdomain("subsub") // subsub.www.mydomain.com + // subsub.Get("/", func(ctx iris.Context) { ctx.Writef("subdomain is: " + ctx.Subdomain()) }) + // app.SubdomainRedirect(subsub, 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: diff --git a/core/router/path.go b/core/router/path.go index 5311e4d2..51b1716f 100644 --- a/core/router/path.go +++ b/core/router/path.go @@ -129,7 +129,10 @@ func hasSubdomain(s string) bool { // if not start with "/" then it should be something else, // we don't assume anything else but subdomain. slashIdx := strings.IndexByte(s, '/') - return slashIdx > 0 || s[0] == SubdomainPrefix[0] || (len(s) >= 2 && s[0:2] == SubdomainWildcardIndicator) + return slashIdx > 0 || // for route paths + s[0] == SubdomainPrefix[0] || // for route paths + (len(s) >= 2 && s[0:2] == SubdomainWildcardIndicator) || // for party rel path or route paths + (len(s) >= 2 && slashIdx != 0 && s[len(s)-1] == '.') // for party rel, i.e www., or subsub.www. } // splitSubdomainAndPath checks if the path has subdomain and if it's diff --git a/iris.go b/iris.go index 846a2c29..40431e3c 100644 --- a/iris.go +++ b/iris.go @@ -705,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 gives 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) } From 68acbf3f31e0fc29cc9c590bc855cac36ea7a672 Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Sun, 21 Jan 2018 07:14:51 +0200 Subject: [PATCH 04/17] fix https://github.com/kataras/iris/issues/879 by @martinnagelberg Former-commit-id: 171480e3cb0b172d8f28eb95a36c97f926836810 --- FAQ.md | 2 +- middleware/basicauth/basicauth.go | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/FAQ.md b/FAQ.md index 72e6f57a..95040630 100644 --- a/FAQ.md +++ b/FAQ.md @@ -63,7 +63,7 @@ You can find all type aliases and their original package import statements at th ## Active development mode -Iris may have reached version 8, but we're not stopping there. We have many feature ideas on our board that we're anxious to add and other innovative web development solutions that we're planning to build into Iris. +Iris may have reached version 10, but we're not stopping there. We have many feature ideas on our board that we're anxious to add and other innovative web development solutions that we're planning to build into Iris. ## Can I find a job if I learn how to use Iris? diff --git a/middleware/basicauth/basicauth.go b/middleware/basicauth/basicauth.go index 6ebf8de0..be3cebec 100644 --- a/middleware/basicauth/basicauth.go +++ b/middleware/basicauth/basicauth.go @@ -42,6 +42,7 @@ func New(c Config) context.Handler { config.Realm = c.Realm } config.Users = c.Users + config.Expires = c.Expires b := &basicAuthMiddleware{config: config} b.init() From d4b09987c4432e19f29152a64228e9e6d426f701 Mon Sep 17 00:00:00 2001 From: Kimmo Hintikka Date: Mon, 22 Jan 2018 11:04:33 +0000 Subject: [PATCH 05/17] corrected spelling for example docs Corrected spelling from simmilate to simulate Former-commit-id: f672aa340fd53119defa89da0d93757dc0b0152b --- _examples/miscellaneous/recover/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_examples/miscellaneous/recover/main.go b/_examples/miscellaneous/recover/main.go index a366f3b5..9b5b7dc5 100644 --- a/_examples/miscellaneous/recover/main.go +++ b/_examples/miscellaneous/recover/main.go @@ -12,7 +12,7 @@ func main() { app.Use(recover.New()) i := 0 - // let's simmilate a panic every next request + // let's simulate a panic every next request app.Get("/", func(ctx iris.Context) { i++ if i%2 == 0 { From 4b4dbb04af405be7302c3a93ddbf03dd8c1a37d7 Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Mon, 22 Jan 2018 15:42:19 +0200 Subject: [PATCH 06/17] add an `app.View` example for parsing and writing templates outside of the HTTP scope(context.View) Former-commit-id: e65d8ece521c778dedf45cf2f522383c26b9901b --- HISTORY.md | 6 +-- HISTORY_GR.md | 6 +-- _examples/README.md | 2 +- _examples/view/write-to/main.go | 43 +++++++++++++++++++ .../view/write-to/views/email/simple.html | 1 + .../view/write-to/views/shared/email.html | 6 +++ cache/client/handler.go | 23 +++++++++- 7 files changed, 79 insertions(+), 8 deletions(-) create mode 100644 _examples/view/write-to/main.go create mode 100644 _examples/view/write-to/views/email/simple.html create mode 100644 _examples/view/write-to/views/shared/email.html diff --git a/HISTORY.md b/HISTORY.md index 9532c2f0..1ae89303 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -17,7 +17,7 @@ Developers are not forced to upgrade if they don't really need it. Upgrade whene **How to upgrade**: Open your command-line and execute this command: `go get -u github.com/kataras/iris` or let the automatic updater do that for you. -# Tu, 16 Jenuary 2018 | v10.0.2 +# Tu, 16 January 2018 | v10.0.2 ## Security | `iris.AutoTLS` @@ -28,7 +28,7 @@ Read more at: - https://letsencrypt.status.io/pages/incident/55957a99e800baa4470002da/5a55777ed9a9c1024c00b241 - https://github.com/golang/crypto/commit/13931e22f9e72ea58bb73048bc752b48c6d4d4ac -# Mo, 15 Jenuary 2018 | v10.0.1 +# Mo, 15 January 2018 | v10.0.1 Not any serious problems were found to be resolved here but one, the first one which is important for devs that used the [cache](cache) package. @@ -57,7 +57,7 @@ Not any serious problems were found to be resolved here but one, the first one w 1. [A Todo MVC Application using Iris and Vue.js](https://hackernoon.com/a-todo-mvc-application-using-iris-and-vue-js-5019ff870064) 2. [A Hasura starter project with a ready to deploy Golang hello-world web app with IRIS](bit.ly/2lmKaAZ) -# Mo, 01 Jenuary 2018 | v10.0.0 +# Mo, 01 January 2018 | v10.0.0 We must thanks [Mrs. Diana](https://www.instagram.com/merry.dii/) for our awesome new [logo](https://iris-go.com/images/icon.svg)! diff --git a/HISTORY_GR.md b/HISTORY_GR.md index fcad3d94..e3fd5492 100644 --- a/HISTORY_GR.md +++ b/HISTORY_GR.md @@ -17,7 +17,7 @@ **Πώς να αναβαθμίσετε**: Ανοίξτε την γραμμή εντολών σας και εκτελέστε αυτήν την εντολή: `go get -u github.com/kataras/iris` ή αφήστε το αυτόματο updater να το κάνει αυτό για σας. -# Tu, 16 Jenuary 2018 | v10.0.2 +# Tu, 16 January 2018 | v10.0.2 ## Ασφάλεια | `iris.AutoTLS` @@ -28,7 +28,7 @@ - https://letsencrypt.status.io/pages/incident/55957a99e800baa4470002da/5a55777ed9a9c1024c00b241 - https://github.com/golang/crypto/commit/13931e22f9e72ea58bb73048bc752b48c6d4d4ac -# Mo, 15 Jenuary 2018 | v10.0.1 +# Mo, 15 January 2018 | v10.0.1 - διόρθωση του cache handler που δεν δούλευε όπως έπρεπε όταν γινόταν εγγραφή σε πάνω από ένα handler, παλιότερα ήταν ένα cache handler προς ένα route handler, τώρα το ίδιο handler μπορεί να καταχωρηθεί σε όσα route handlers θέλετε https://github.com/kataras/iris/pull/852, όπως είχε αναφερθεί στο https://github.com/kataras/iris/issues/850 - συγχώνευση PR https://github.com/kataras/iris/pull/862 @@ -54,7 +54,7 @@ 1. [A Todo MVC Application using Iris and Vue.js](https://hackernoon.com/a-todo-mvc-application-using-iris-and-vue-js-5019ff870064) 2. [A Hasura starter project with a ready to deploy Golang hello-world web app with IRIS](bit.ly/2lmKaAZ) -# Mo, 01 Jenuary 2018 | v10.0.0 +# Mo, 01 January 2018 | v10.0.0 Πρέπει να ευχαριστήσουμε την [Κυρία Diana](https://www.instagram.com/merry.dii/) για το νέο μας [λογότυπο](https://iris-go.com/images/icon.svg)! diff --git a/_examples/README.md b/_examples/README.md index 87fbf890..3076b4c5 100644 --- a/_examples/README.md +++ b/_examples/README.md @@ -294,7 +294,7 @@ Follow the examples below, - [The `url` tmpl func](view/template_html_4/main.go) - [Inject Data Between Handlers](view/context-view-data/main.go) - [Embedding Templates Into App Executable File](view/embedding-templates-into-app/main.go) - +- [Write to a custom `io.Writer`](view/write-to) You can serve [quicktemplate](https://github.com/valyala/quicktemplate) and [hero templates](https://github.com/shiyanhui/hero/hero) files too, simply by using the `context#ResponseWriter`, take a look at the [http_responsewriter/quicktemplate](http_responsewriter/quicktemplate) and [http_responsewriter/herotemplate](http_responsewriter/herotemplate) examples. diff --git a/_examples/view/write-to/main.go b/_examples/view/write-to/main.go new file mode 100644 index 00000000..19adbe72 --- /dev/null +++ b/_examples/view/write-to/main.go @@ -0,0 +1,43 @@ +package main + +import ( + "os" + + "github.com/kataras/iris" +) + +type mailData struct { + Title string + Body string + RefTitle string + RefLink string +} + +func main() { + app := iris.New() + app.Logger().SetLevel("debug") + app.RegisterView(iris.HTML("./views", ".html")) + + // you need to call `app.Build` manually before using the `app.View` func, + // so templates are built in that state. + app.Build() + + // Or a string-buffered writer to use its body to send an e-mail + // for sending e-mails you can use the https://github.com/kataras/go-mailer + // or any other third-party package you like. + // + // The template's parsed result will be written to that writer. + writer := os.Stdout + err := app.View(writer, "email/simple.html", "shared/email.html", mailData{ + Title: "This is my e-mail title", + Body: "This is my e-mail body", + RefTitle: "Iris web framework", + RefLink: "https://iris-go.com", + }) + + if err != nil { + app.Logger().Errorf("error from app.View: %v", err) + } + + app.Run(iris.Addr(":8080")) +} diff --git a/_examples/view/write-to/views/email/simple.html b/_examples/view/write-to/views/email/simple.html new file mode 100644 index 00000000..6e411194 --- /dev/null +++ b/_examples/view/write-to/views/email/simple.html @@ -0,0 +1 @@ +{{.Body}} \ No newline at end of file diff --git a/_examples/view/write-to/views/shared/email.html b/_examples/view/write-to/views/shared/email.html new file mode 100644 index 00000000..6d5baceb --- /dev/null +++ b/_examples/view/write-to/views/shared/email.html @@ -0,0 +1,6 @@ +

{{.Title}}

+

+ {{yield}} +

+ +{{.RefTitle}} diff --git a/cache/client/handler.go b/cache/client/handler.go index 9b41e538..389d0336 100644 --- a/cache/client/handler.go +++ b/cache/client/handler.go @@ -66,6 +66,8 @@ var emptyHandler = func(ctx context.Context) { ctx.StopExecution() } +///TODO: debug this and re-run the parallel tests on larger scale, +// because I think we have a bug here when `core/router#StaticWeb` is used after this middleware. func (h *Handler) ServeHTTP(ctx context.Context) { // check for pre-cache validators, if at least one of them return false // for this specific request, then skip the whole cache @@ -83,10 +85,16 @@ func (h *Handler) ServeHTTP(ctx context.Context) { return } + scheme := "http" + if ctx.Request().TLS != nil { + scheme = "https" + } + var ( response *entry.Response valid = false - key = ctx.Path() + // unique per subdomains and paths with different url query. + key = scheme + ctx.Host() + ctx.Request().URL.RequestURI() ) h.mu.RLock() @@ -99,6 +107,9 @@ func (h *Handler) ServeHTTP(ctx context.Context) { response, valid = e.Response() } else { // create the entry now. + // fmt.Printf("create new cache entry\n") + // fmt.Printf("key: %s\n", key) + e = entry.NewEntry(h.expiration) h.mu.Lock() h.entries[key] = e @@ -132,6 +143,11 @@ func (h *Handler) ServeHTTP(ctx context.Context) { // given expiration was not valid then check for GetMaxAge & // update the response & release the recorder e.Reset(recorder.StatusCode(), recorder.Header().Get(cfg.ContentTypeHeader), body, GetMaxAge(ctx.Request())) + + // fmt.Printf("reset cache entry\n") + // fmt.Printf("key: %s\n", key) + // fmt.Printf("content type: %s\n", recorder.Header().Get(cfg.ContentTypeHeader)) + // fmt.Printf("body len: %d\n", len(body)) return } @@ -139,4 +155,9 @@ func (h *Handler) ServeHTTP(ctx context.Context) { ctx.ContentType(response.ContentType()) ctx.StatusCode(response.StatusCode()) ctx.Write(response.Body()) + + // fmt.Printf("key: %s\n", key) + // fmt.Printf("write content type: %s\n", response.ContentType()) + // fmt.Printf("write body len: %d\n", len(response.Body())) + } From bf13f7648afdcb193f2b3f9af5fcfeaa15f4ef3a Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Thu, 25 Jan 2018 03:16:49 +0200 Subject: [PATCH 07/17] add vscode extension link and badge | Some internal improvements (not completed yet) Former-commit-id: 9bc94e90a2780ee81f8188509d98063fb3f2924b --- FAQ.md | 10 +- README.md | 2 +- README_GR.md | 2 +- README_RU.md | 2 +- README_ZH.md | 2 +- .../casbin/wrapper/main_test.go | 4 +- _examples/experimental-handlers/csrf/main.go | 2 +- _examples/subdomains/redirect/main_test.go | 5 +- _examples/subdomains/www/main.go | 18 +- _examples/subdomains/www/main_test.go | 4 +- context/context.go | 168 +++++++++++++----- core/router/api_builder.go | 78 ++++---- core/router/fs.go | 167 +++++++---------- core/router/party.go | 18 +- hero/func_result_test.go | 23 +++ httptest/httptest.go | 7 +- 16 files changed, 293 insertions(+), 219 deletions(-) diff --git a/FAQ.md b/FAQ.md index 95040630..63d89d15 100644 --- a/FAQ.md +++ b/FAQ.md @@ -10,6 +10,14 @@ Add a `badge` to your open-source projects powered by [Iris](https://iris-go.com > The badge is optionally, of course, it is just a simple and fast way to support Iris. The badge is work of a third-party, taken from https://github.com/blob-go/blob-go which was published by our friend @clover113 and we loved it<3 +## Editors & IDEs Extensions + +### Visual Studio Code + + + +> Please feel free to list your own Iris extension(s) here by [PR](https://github.com/kataras/iris/pulls) + ## How to upgrade ```sh @@ -18,7 +26,7 @@ go get -u github.com/kataras/iris ## Learning -More than 50 practical examples, tutorials and articles at: +More than 100 practical examples, tutorials and articles at: - https://github.com/kataras/iris/tree/master/_examples - https://github.com/iris-contrib/examples diff --git a/README.md b/README.md index 45aa4116..fdaf6181 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ -[![build status](https://img.shields.io/travis/kataras/iris/master.svg?style=flat-square)](https://travis-ci.org/kataras/iris) [![report card](https://img.shields.io/badge/report%20card-a%2B-ff3333.svg?style=flat-square)](http://goreportcard.com/report/kataras/iris) [![chat](https://img.shields.io/badge/community-%20chat-00BCD4.svg?style=flat-square)](https://kataras.rocket.chat/channel/iris) [![view examples](https://img.shields.io/badge/learn%20by-examples-0077b3.svg?style=flat-square)](_examples/) [![release](https://img.shields.io/badge/release%20-v10.0-0077b3.svg?style=flat-square)](https://github.com/kataras/iris/releases) +[![build status](https://img.shields.io/travis/kataras/iris/master.svg?style=flat-square)](https://travis-ci.org/kataras/iris) [![report card](https://img.shields.io/badge/report%20card-a%2B-ff3333.svg?style=flat-square)](http://goreportcard.com/report/kataras/iris) [![vscode-iris](https://img.shields.io/badge/ext%20-vscode-0c77e3.svg?style=flat-square)](https://github.com/kataras/vscode-iris) [![chat](https://img.shields.io/badge/community-%20chat-00BCD4.svg?style=flat-square)](https://kataras.rocket.chat/channel/iris) [![view examples](https://img.shields.io/badge/learn%20by-examples-0077b3.svg?style=flat-square)](_examples/) [![release](https://img.shields.io/badge/release%20-v10.0-0077b3.svg?style=flat-square)](https://github.com/kataras/iris/releases) Iris is a fast, simple yet fully featured and very efficient web framework for Go. diff --git a/README_GR.md b/README_GR.md index cc6d0e14..8eb17c9b 100644 --- a/README_GR.md +++ b/README_GR.md @@ -2,7 +2,7 @@ -[![build status](https://img.shields.io/travis/kataras/iris/master.svg?style=flat-square)](https://travis-ci.org/kataras/iris) [![report card](https://img.shields.io/badge/report%20card-a%2B-ff3333.svg?style=flat-square)](http://goreportcard.com/report/kataras/iris) [![chat](https://img.shields.io/badge/community-%20chat-00BCD4.svg?style=flat-square)](https://kataras.rocket.chat/channel/iris) [![view examples](https://img.shields.io/badge/learn%20by-examples-0077b3.svg?style=flat-square)](_examples/) [![release](https://img.shields.io/badge/release%20-v10.0-0077b3.svg?style=flat-square)](https://github.com/kataras/iris/releases) +[![build status](https://img.shields.io/travis/kataras/iris/master.svg?style=flat-square)](https://travis-ci.org/kataras/iris) [![report card](https://img.shields.io/badge/report%20card-a%2B-ff3333.svg?style=flat-square)](http://goreportcard.com/report/kataras/iris) [![vscode-iris](https://img.shields.io/badge/ext%20-vscode-0c77e3.svg?style=flat-square)](https://github.com/kataras/vscode-iris) [![chat](https://img.shields.io/badge/community-%20chat-00BCD4.svg?style=flat-square)](https://kataras.rocket.chat/channel/iris) [![view examples](https://img.shields.io/badge/learn%20by-examples-0077b3.svg?style=flat-square)](_examples/) [![release](https://img.shields.io/badge/release%20-v10.0-0077b3.svg?style=flat-square)](https://github.com/kataras/iris/releases) Το Iris είναι ένα γρήγορο, απλό αλλά και πλήρως λειτουργικό και πολύ αποδοτικό web framework για τη Go. diff --git a/README_RU.md b/README_RU.md index 85715d81..520cf0ca 100644 --- a/README_RU.md +++ b/README_RU.md @@ -2,7 +2,7 @@ -[![build status](https://img.shields.io/travis/kataras/iris/master.svg?style=flat-square)](https://travis-ci.org/kataras/iris) [![report card](https://img.shields.io/badge/report%20card-a%2B-ff3333.svg?style=flat-square)](http://goreportcard.com/report/kataras/iris) [![chat](https://img.shields.io/badge/community-%20chat-00BCD4.svg?style=flat-square)](https://kataras.rocket.chat/channel/iris) [![view examples](https://img.shields.io/badge/learn%20by-examples-0077b3.svg?style=flat-square)](_examples/) [![release](https://img.shields.io/badge/release%20-v10.0-0077b3.svg?style=flat-square)](https://github.com/kataras/iris/releases) +[![build status](https://img.shields.io/travis/kataras/iris/master.svg?style=flat-square)](https://travis-ci.org/kataras/iris) [![report card](https://img.shields.io/badge/report%20card-a%2B-ff3333.svg?style=flat-square)](http://goreportcard.com/report/kataras/iris) [![vscode-iris](https://img.shields.io/badge/ext%20-vscode-0c77e3.svg?style=flat-square)](https://github.com/kataras/vscode-iris) [![chat](https://img.shields.io/badge/community-%20chat-00BCD4.svg?style=flat-square)](https://kataras.rocket.chat/channel/iris) [![view examples](https://img.shields.io/badge/learn%20by-examples-0077b3.svg?style=flat-square)](_examples/) [![release](https://img.shields.io/badge/release%20-v10.0-0077b3.svg?style=flat-square)](https://github.com/kataras/iris/releases) Iris - это быстрая, простая, но полнофункциональная и очень эффективная веб-платформа для Go. diff --git a/README_ZH.md b/README_ZH.md index ec21ab48..bd58ada6 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -2,7 +2,7 @@ -[![build status](https://img.shields.io/travis/kataras/iris/master.svg?style=flat-square)](https://travis-ci.org/kataras/iris) [![report card](https://img.shields.io/badge/report%20card-a%2B-ff3333.svg?style=flat-square)](http://goreportcard.com/report/kataras/iris) [![chat](https://img.shields.io/badge/community-%20chat-00BCD4.svg?style=flat-square)](https://kataras.rocket.chat/channel/iris) [![view examples](https://img.shields.io/badge/learn%20by-examples-0077b3.svg?style=flat-square)](_examples/) [![release](https://img.shields.io/badge/release%20-v10.0-0077b3.svg?style=flat-square)](https://github.com/kataras/iris/releases) +[![build status](https://img.shields.io/travis/kataras/iris/master.svg?style=flat-square)](https://travis-ci.org/kataras/iris) [![report card](https://img.shields.io/badge/report%20card-a%2B-ff3333.svg?style=flat-square)](http://goreportcard.com/report/kataras/iris) [![vscode-iris](https://img.shields.io/badge/ext%20-vscode-0c77e3.svg?style=flat-square)](https://github.com/kataras/vscode-iris) [![chat](https://img.shields.io/badge/community-%20chat-00BCD4.svg?style=flat-square)](https://kataras.rocket.chat/channel/iris) [![view examples](https://img.shields.io/badge/learn%20by-examples-0077b3.svg?style=flat-square)](_examples/) [![release](https://img.shields.io/badge/release%20-v10.0-0077b3.svg?style=flat-square)](https://github.com/kataras/iris/releases) Iris 是一款超快、简洁高效的 Go 语言 Web开发框架。 diff --git a/_examples/experimental-handlers/casbin/wrapper/main_test.go b/_examples/experimental-handlers/casbin/wrapper/main_test.go index b107bfda..152a8108 100644 --- a/_examples/experimental-handlers/casbin/wrapper/main_test.go +++ b/_examples/experimental-handlers/casbin/wrapper/main_test.go @@ -9,7 +9,7 @@ import ( func TestCasbinWrapper(t *testing.T) { app := newApp() - e := httptest.New(t, app, httptest.Debug(true)) + e := httptest.New(t, app) type ttcasbin struct { username string @@ -43,7 +43,6 @@ func TestCasbinWrapper(t *testing.T) { check(e, tt.method, tt.path, tt.username, tt.status) } - println("ADMIN ROLES") ttAdmin := []ttcasbin{ {"cathrin", "/dataset1/item", "GET", 200}, {"cathrin", "/dataset1/item", "POST", 200}, @@ -57,7 +56,6 @@ func TestCasbinWrapper(t *testing.T) { check(e, tt.method, tt.path, tt.username, tt.status) } - println("ADMIN ROLE FOR cathrin DELETED") Enforcer.DeleteRolesForUser("cathrin") ttAdminDeleted := []ttcasbin{ diff --git a/_examples/experimental-handlers/csrf/main.go b/_examples/experimental-handlers/csrf/main.go index 3e2f27de..c9e6a7d5 100644 --- a/_examples/experimental-handlers/csrf/main.go +++ b/_examples/experimental-handlers/csrf/main.go @@ -42,7 +42,7 @@ func getSignupForm(ctx iris.Context) { // views/signup.html just needs a {{ .csrfField }} template tag for // csrf.TemplateField to inject the CSRF token into. Easy! ctx.ViewData(csrf.TemplateTag, csrf.TemplateField(ctx)) - ctx.View("views/user/signup.html") + ctx.View("user/signup.html") // We could also retrieve the token directly from csrf.Token(r) and // set it in the request header - ctx.GetHeader("X-CSRF-Token", token) diff --git a/_examples/subdomains/redirect/main_test.go b/_examples/subdomains/redirect/main_test.go index cc4f931d..4f2a62cf 100644 --- a/_examples/subdomains/redirect/main_test.go +++ b/_examples/subdomains/redirect/main_test.go @@ -24,10 +24,7 @@ func TestSubdomainRedirectWWW(t *testing.T) { } for _, test := range tests { - req := e.GET(test.path) - // req.WithURL("http://www." + root) - - req.Expect().Status(httptest.StatusOK).Body().Equal(test.response) + e.GET(test.path).Expect().Status(httptest.StatusOK).Body().Equal(test.response) } } diff --git a/_examples/subdomains/www/main.go b/_examples/subdomains/www/main.go index 26dc2672..c41e343d 100644 --- a/_examples/subdomains/www/main.go +++ b/_examples/subdomains/www/main.go @@ -30,19 +30,19 @@ func newApp() *iris.Application { www := app.Party("www.") { + // 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" and subdomains: + currentRoutes := app.GetRoutes() + // Register them to the www subdomain/vhost as well: + for _, r := range currentRoutes { + www.Handle(r.Method, r.Tmpl().Src, r.Handlers...) + } + // 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: - 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(;)). diff --git a/_examples/subdomains/www/main_test.go b/_examples/subdomains/www/main_test.go index f12c6bb8..cd698384 100644 --- a/_examples/subdomains/www/main_test.go +++ b/_examples/subdomains/www/main_test.go @@ -41,13 +41,13 @@ func TestSubdomainWWW(t *testing.T) { } host := "localhost:1111" - e := httptest.New(t, app, httptest.URL("http://"+host)) + e := httptest.New(t, app, httptest.URL("http://"+host), httptest.Debug(false)) for _, test := range tests { req := e.Request(test.method, test.path) if subdomain := test.subdomain; subdomain != "" { - req.WithURL("http://" + subdomain + "." + host) + req = req.WithURL("http://" + subdomain + "." + host) } req.Expect(). diff --git a/context/context.go b/context/context.go index a776143b..ed58bda7 100644 --- a/context/context.go +++ b/context/context.go @@ -891,18 +891,6 @@ type Context interface { var _ Context = (*context)(nil) -// Next calls all the next handler from the handlers chain, -// it should be used inside a middleware. -func Next(ctx Context) { - if ctx.IsStopped() { - return - } - if n, handlers := ctx.HandlerIndex(-1)+1, ctx.Handlers(); n < len(handlers) { - ctx.HandlerIndex(n) - handlers[n](ctx) - } -} - // Do calls the SetHandlers(handlers) // and executes the first handler, // handlers should not be empty. @@ -1159,17 +1147,38 @@ func (ctx *context) HandlerName() string { return HandlerName(ctx.handlers[ctx.currentHandlerIndex]) } -// Do sets the handler index to zero, executes the first handler -// and the rest of the Handlers if ctx.Next() was called. -// func (ctx *context) Do() { -// ctx.currentHandlerIndex = 0 -// ctx.handlers[0](ctx) // it calls this *context -// } // -> replaced with inline on router.go +// Next is the function that executed when `ctx.Next()` is called. +// It can be changed to a customized one if needed (very advanced usage). +// +// See `DefaultNext` for more information about this and why it's exported like this. +var Next = DefaultNext ///TODO: add an example for this usecase, i.e describe handlers and skip only file handlers. + +// DefaultNext is the default function that executed on each middleware if `ctx.Next()` +// is called. +// +// DefaultNext calls the next handler from the handlers chain by registration order, +// it should be used inside a middleware. +// +// It can be changed to a customized one if needed (very advanced usage). +// +// Developers are free to customize the whole or part of the Context's implementation +// by implementing a new `context.Context` (see https://github.com/kataras/iris/tree/master/_examples/routing/custom-context) +// or by just override the `context.Next` package-level field, `context.DefaultNext` is exported +// in order to be able for developers to merge your customized version one with the default behavior as well. +func DefaultNext(ctx Context) { + if ctx.IsStopped() { + return + } + if n, handlers := ctx.HandlerIndex(-1)+1, ctx.Handlers(); n < len(handlers) { + ctx.HandlerIndex(n) + handlers[n](ctx) + } +} // Next calls all the next handler from the handlers chain, // it should be used inside a middleware. // -// Note: Custom context should override this method in order to be able to pass its own context.context implementation. +// Note: Custom context should override this method in order to be able to pass its own context.Context implementation. func (ctx *context) Next() { // or context.Next(ctx) Next(ctx) } @@ -2046,29 +2055,111 @@ var ( varyHeaderKey = "Vary" ) -// staticCachePassed checks the IfModifiedSince header and -// returns true if (client-side) duration has expired -func (ctx *context) staticCachePassed(modtime time.Time) bool { - if t, err := time.Parse(ctx.Application().ConfigurationReadOnly().GetTimeFormat(), ctx.GetHeader(ifModifiedSinceHeaderKey)); err == nil && modtime.Before(t.Add(StaticCacheDuration)) { - ctx.writer.Header().Del(contentTypeHeaderKey) - ctx.writer.Header().Del(contentLengthHeaderKey) - ctx.StatusCode(http.StatusNotModified) - return true +var unixEpochTime = time.Unix(0, 0) + +// IsZeroTime reports whether t is obviously unspecified (either zero or Unix()=0). +func IsZeroTime(t time.Time) bool { + return t.IsZero() || t.Equal(unixEpochTime) +} + +// ParseTime parses a time header (such as the Date: header), +// trying each forth formats (or three if Application's configuration's TimeFormat is defaulted) +// that are allowed by HTTP/1.1: +// Application's configuration's TimeFormat or/and http.TimeFormat, +// time.RFC850, and time.ANSIC. +// +// Look `context#FormatTime` for the opossite operation (Time to string). +var ParseTime = func(ctx Context, text string) (t time.Time, err error) { + t, err = time.Parse(ctx.Application().ConfigurationReadOnly().GetTimeFormat(), text) + if err != nil { + return http.ParseTime(text) } - return false + + return +} + +// FormatTime returns a textual representation of the time value formatted +// according to the Application's configuration's TimeFormat field +// which defines the format. +// +// Look `context#ParseTime` for the opossite operation (string to Time). +var FormatTime = func(ctx Context, t time.Time) string { + return t.Format(ctx.Application().ConfigurationReadOnly().GetTimeFormat()) +} + +// SetLastModified sets the "Last-Modified" based on the "modtime" input. +// If "modtime" is zero then it does nothing. +// +// It's mostly internally on core/router and context packages. +func SetLastModified(ctx Context, modtime time.Time) { + if !IsZeroTime(modtime) { + ctx.Header(lastModifiedHeaderKey, FormatTime(ctx, modtime)) // or modtime.UTC()? + } +} + +// CheckIfModifiedSince checks if the response is modified since the "modtime". +// Note that it has nothing to do with server-side caching. +// It does those checks by checking if the "If-Modified-Since" request header +// sent by client or a previous server response header +// (e.g with WriteWithExpiration or StaticEmbedded or Favicon etc.) +// is a valid one and it's before the "modtime". +// +// A check for !modtime && err == nil is necessary to make sure that +// it's not modified since, because it may return false but without even +// had the chance to check the client-side (request) header due to some errors, +// like the HTTP Method is not "GET" or "HEAD" or if the "modtime" is zero +// or if parsing time from the header failed. +// +// It's mostly used internally, e.g. `context#WriteWithExpiration`. +func CheckIfModifiedSince(ctx Context, modtime time.Time) (bool, error) { + if method := ctx.Method(); method != http.MethodGet && method != http.MethodHead { + return false, errors.New("skip: method") + } + ims := ctx.GetHeader(ifModifiedSinceHeaderKey) + if ims == "" || IsZeroTime(modtime) { + return false, errors.New("skip: zero time") + } + t, err := ParseTime(ctx, ims) + if err != nil { + return false, errors.New("skip: " + err.Error()) + } + // sub-second precision, so + // use mtime < t+1s instead of mtime <= t to check for unmodified. + if modtime.Before(t.Add(1 * time.Second)) { + return false, nil + } + return true, nil +} + +// WriteNotModified sends a 304 "Not Modified" status code to the client, +// it makes sure that the content type, the content length headers +// and any "ETag" are removed before the response sent. +// +// It's mostly used internally on core/router/fs.go and context methods. +func WriteNotModified(ctx Context) { + // RFC 7232 section 4.1: + // a sender SHOULD NOT generate representation metadata other than the + // above listed fields unless said metadata exists for the purpose of + // guiding cache updates (e.g.," Last-Modified" might be useful if the + // response does not have an ETag field). + h := ctx.ResponseWriter().Header() + delete(h, contentTypeHeaderKey) + delete(h, contentLengthHeaderKey) + if h.Get("Etag") != "" { + delete(h, lastModifiedHeaderKey) + } + ctx.StatusCode(http.StatusNotModified) } // WriteWithExpiration like Write but it sends with an expiration datetime // which is refreshed every package-level `StaticCacheDuration` field. func (ctx *context) WriteWithExpiration(body []byte, modtime time.Time) (int, error) { - - if ctx.staticCachePassed(modtime) { + if modified, err := CheckIfModifiedSince(ctx, modtime); !modified && err == nil { + WriteNotModified(ctx) return 0, nil } - modtimeFormatted := modtime.UTC().Format(ctx.Application().ConfigurationReadOnly().GetTimeFormat()) - ctx.Header(lastModifiedHeaderKey, modtimeFormatted) - + SetLastModified(ctx, modtime) return ctx.writer.Write(body) } @@ -2658,16 +2749,13 @@ const ( // You can define your own "Content-Type" header also, after this function call // Doesn't implements resuming (by range), use ctx.SendFile instead func (ctx *context) ServeContent(content io.ReadSeeker, filename string, modtime time.Time, gzipCompression bool) error { - if t, err := time.Parse(ctx.Application().ConfigurationReadOnly().GetTimeFormat(), ctx.GetHeader(ifModifiedSinceHeaderKey)); err == nil && modtime.Before(t.Add(1*time.Second)) { - ctx.writer.Header().Del(contentTypeHeaderKey) - ctx.writer.Header().Del(contentLengthHeaderKey) - ctx.StatusCode(http.StatusNotModified) + if modified, err := CheckIfModifiedSince(ctx, modtime); !modified && err == nil { + WriteNotModified(ctx) return nil } ctx.ContentType(filename) - ctx.writer.Header().Set(lastModifiedHeaderKey, modtime.UTC().Format(ctx.Application().ConfigurationReadOnly().GetTimeFormat())) - ctx.StatusCode(http.StatusOK) + SetLastModified(ctx, modtime) var out io.Writer if gzipCompression && ctx.ClientSupportsGzip() { ctx.writer.Header().Add(varyHeaderKey, acceptEncodingHeaderKey) @@ -2680,7 +2768,7 @@ func (ctx *context) ServeContent(content io.ReadSeeker, filename string, modtime out = ctx.writer } _, err := io.Copy(out, content) - return errServeContent.With(err) + return errServeContent.With(err) ///TODO: add an int64 as return value for the content length written like other writers or let it as it's in order to keep the stable api? } // ServeFile serves a view file, to send a file ( zip for example) to the client you should use the SendFile(serverfilename,clientfilename) diff --git a/core/router/api_builder.go b/core/router/api_builder.go index 4fb3bb46..5864aa1e 100644 --- a/core/router/api_builder.go +++ b/core/router/api_builder.go @@ -629,7 +629,6 @@ func (api *APIBuilder) Favicon(favPath string, requestPath ...string) *Route { return api.Favicon(path.Join(favPath, "favicon.ico")) } - cType := TypeByFilename(favPath) // copy the bytes here in order to cache and not read the ico on each request. cacheFav := make([]byte, fi.Size()) if _, err = f.Read(cacheFav); err != nil { @@ -641,25 +640,14 @@ func (api *APIBuilder) Favicon(favPath string, requestPath ...string) *Route { Format(favPath, "favicon: couldn't read the data bytes for file: "+err.Error())) return nil } - modtime := "" + + modtime := time.Now() + cType := TypeByFilename(favPath) h := func(ctx context.Context) { - if modtime == "" { - modtime = fi.ModTime().UTC().Format(ctx.Application().ConfigurationReadOnly().GetTimeFormat()) - } - if t, err := time.Parse(ctx.Application().ConfigurationReadOnly().GetTimeFormat(), ctx.GetHeader(ifModifiedSinceHeaderKey)); err == nil && fi.ModTime().Before(t.Add(StaticCacheDuration)) { - - ctx.ResponseWriter().Header().Del(contentTypeHeaderKey) - ctx.ResponseWriter().Header().Del(contentLengthHeaderKey) - ctx.StatusCode(http.StatusNotModified) - return - } - - ctx.ResponseWriter().Header().Set(contentTypeHeaderKey, cType) - ctx.ResponseWriter().Header().Set(lastModifiedHeaderKey, modtime) - ctx.StatusCode(http.StatusOK) - if _, err := ctx.Write(cacheFav); err != nil { - // ctx.Application().Logger().Infof("error while trying to serve the favicon: %s", err.Error()) + ctx.ContentType(cType) + if _, err := ctx.WriteWithExpiration(cacheFav, modtime); err != nil { ctx.StatusCode(http.StatusInternalServerError) + ctx.Application().Logger().Debugf("while trying to serve the favicon: %s", err.Error()) } } @@ -698,16 +686,16 @@ func (api *APIBuilder) StaticWeb(requestPath string, systemPath string) *Route { handler := func(ctx context.Context) { h(ctx) - if ctx.GetStatusCode() >= 200 && ctx.GetStatusCode() < 400 { - // re-check the content type here for any case, - // although the new code does it automatically but it's good to have it here. - if _, exists := ctx.ResponseWriter().Header()["Content-Type"]; !exists { - if fname := ctx.Params().Get(paramName); fname != "" { - cType := TypeByFilename(fname) - ctx.ContentType(cType) - } - } - } + // if ctx.GetStatusCode() >= 200 && ctx.GetStatusCode() < 400 { + // // re-check the content type here for any case, + // // although the new code does it automatically but it's good to have it here. + // if _, exists := ctx.ResponseWriter().Header()["Content-Type"]; !exists { + // if fname := ctx.Params().Get(paramName); fname != "" { + // cType := TypeByFilename(fname) + // ctx.ContentType(cType) + // } + // } + // } } requestPath = joinPath(requestPath, WildcardParam(paramName)) @@ -791,16 +779,20 @@ func (api *APIBuilder) FireErrorCode(ctx context.Context) { api.errorCodeHandlers.Fire(ctx) } -// Layout oerrides the parent template layout with a more specific layout for this Party -// returns this Party, to continue as normal +// Layout overrides the parent template layout with a more specific layout for this Party. +// It returns the current Party. +// +// The "tmplLayoutFile" should be a relative path to the templates dir. // Usage: +// // app := iris.New() +// app.RegisterView(iris.$VIEW_ENGINE("./views", ".$extension")) // my := app.Party("/my").Layout("layouts/mylayout.html") -// { -// my.Get("/", func(ctx context.Context) { -// ctx.MustRender("page1.html", nil) -// }) -// } +// my.Get("/", func(ctx iris.Context) { +// ctx.View("page1.html") +// }) +// +// Examples: https://github.com/kataras/iris/tree/master/_examples/view func (api *APIBuilder) Layout(tmplLayoutFile string) Party { api.Use(func(ctx context.Context) { ctx.ViewLayout(tmplLayoutFile) @@ -811,14 +803,14 @@ func (api *APIBuilder) Layout(tmplLayoutFile string) Party { } // joinHandlers uses to create a copy of all Handlers and return them in order to use inside the node -func joinHandlers(Handlers1 context.Handlers, Handlers2 context.Handlers) context.Handlers { - nowLen := len(Handlers1) - totalLen := nowLen + len(Handlers2) - // create a new slice of Handlers in order to store all handlers, the already handlers(Handlers) and the new +func joinHandlers(h1 context.Handlers, h2 context.Handlers) context.Handlers { + nowLen := len(h1) + totalLen := nowLen + len(h2) + // create a new slice of Handlers in order to merge the "h1" and "h2" newHandlers := make(context.Handlers, totalLen) - //copy the already Handlers to the just created - copy(newHandlers, Handlers1) - //start from there we finish, and store the new Handlers too - copy(newHandlers[nowLen:], Handlers2) + // copy the already Handlers to the just created + copy(newHandlers, h1) + // start from there we finish, and store the new Handlers too + copy(newHandlers[nowLen:], h2) return newHandlers } diff --git a/core/router/fs.go b/core/router/fs.go index 02f65822..822058cf 100644 --- a/core/router/fs.go +++ b/core/router/fs.go @@ -411,7 +411,7 @@ func detectOrWriteContentType(ctx context.Context, name string, content io.ReadS // content must be seeked to the beginning of the file. // The sizeFunc is called at most once. Its error, if any, is sent in the HTTP response. func serveContent(ctx context.Context, name string, modtime time.Time, sizeFunc func() (int64, error), content io.ReadSeeker) (string, int) /* we could use the TransactionErrResult but prefer not to create new objects for each of the errors on static file handlers*/ { - setLastModified(ctx, modtime) + context.SetLastModified(ctx, modtime) done, rangeReq := checkPreconditions(ctx, modtime) if done { return "", http.StatusNotModified @@ -515,6 +515,17 @@ func serveContent(ctx context.Context, name string, modtime time.Time, sizeFunc return "", code } +func etagEmptyOrStrongMatch(rangeValue string, etagValue string) bool { + etag, _ := scanETag(rangeValue) + if etag != "" { + if etagStrongMatch(etag, etagValue) { + return true + } + return false + } + return true +} + // scanETag determines if a syntactically valid ETag is present at s. If so, // the ETag and remaining text after consuming ETag is returned. Otherwise, // it returns "", "". @@ -595,22 +606,6 @@ func checkIfMatch(ctx context.Context) condResult { return condFalse } -func checkIfUnmodifiedSince(ctx context.Context, modtime time.Time) condResult { - ius := ctx.GetHeader("If-Unmodified-Since") - if ius == "" || isZeroTime(modtime) { - return condNone - } - if t, err := http.ParseTime(ius); err == nil { - // The Date-Modified header truncates sub-second precision, so - // use mtime < t+1s instead of mtime <= t to check for unmodified. - if modtime.Before(t.Add(1 * time.Second)) { - return condTrue - } - return condFalse - } - return condNone -} - func checkIfNoneMatch(ctx context.Context) condResult { inm := ctx.GetHeader("If-None-Match") if inm == "" { @@ -640,86 +635,6 @@ func checkIfNoneMatch(ctx context.Context) condResult { return condTrue } -func checkIfModifiedSince(ctx context.Context, modtime time.Time) condResult { - if ctx.Method() != http.MethodGet && ctx.Method() != http.MethodHead { - return condNone - } - ims := ctx.GetHeader("If-Modified-Since") - if ims == "" || isZeroTime(modtime) { - return condNone - } - t, err := http.ParseTime(ims) - if err != nil { - return condNone - } - // The Date-Modified header truncates sub-second precision, so - // use mtime < t+1s instead of mtime <= t to check for unmodified. - if modtime.Before(t.Add(1 * time.Second)) { - return condFalse - } - return condTrue -} - -func checkIfRange(ctx context.Context, modtime time.Time) condResult { - if ctx.Method() != http.MethodGet { - return condNone - } - ir := ctx.GetHeader("If-Range") - if ir == "" { - return condNone - } - etag, _ := scanETag(ir) - if etag != "" { - if etagStrongMatch(etag, ctx.ResponseWriter().Header().Get("Etag")) { - return condTrue - } - return condFalse - - } - // The If-Range value is typically the ETag value, but it may also be - // the modtime date. See golang.org/issue/8367. - if modtime.IsZero() { - return condFalse - } - t, err := http.ParseTime(ir) - if err != nil { - return condFalse - } - if t.Unix() == modtime.Unix() { - return condTrue - } - return condFalse -} - -var unixEpochTime = time.Unix(0, 0) - -// isZeroTime reports whether t is obviously unspecified (either zero or Unix()=0). -func isZeroTime(t time.Time) bool { - return t.IsZero() || t.Equal(unixEpochTime) -} - -func setLastModified(ctx context.Context, modtime time.Time) { - if !isZeroTime(modtime) { - ctx.Header(lastModifiedHeaderKey, modtime.UTC().Format(ctx.Application().ConfigurationReadOnly().GetTimeFormat())) - } -} - -func writeNotModified(ctx context.Context) { - // RFC 7232 section 4.1: - // a sender SHOULD NOT generate representation metadata other than the - // above listed fields unless said metadata exists for the purpose of - // guiding cache updates (e.g., Last-Modified might be useful if the - // response does not have an ETag field). - h := ctx.ResponseWriter().Header() - delete(h, contentTypeHeaderKey) - - delete(h, contentLengthHeaderKey) - if h.Get("Etag") != "" { - delete(h, "Last-Modified") - } - ctx.StatusCode(http.StatusNotModified) -} - // checkPreconditions evaluates request preconditions and reports whether a precondition // resulted in sending StatusNotModified or StatusPreconditionFailed. func checkPreconditions(ctx context.Context, modtime time.Time) (done bool, rangeHeader string) { @@ -736,28 +651,72 @@ func checkPreconditions(ctx context.Context, modtime time.Time) (done bool, rang switch checkIfNoneMatch(ctx) { case condFalse: if ctx.Method() == http.MethodGet || ctx.Method() == http.MethodHead { - writeNotModified(ctx) + context.WriteNotModified(ctx) return true, "" } ctx.StatusCode(http.StatusPreconditionFailed) return true, "" case condNone: - if checkIfModifiedSince(ctx, modtime) == condFalse { - writeNotModified(ctx) + if modified, err := context.CheckIfModifiedSince(ctx, modtime); !modified && err == nil { + context.WriteNotModified(ctx) return true, "" } } rangeHeader = ctx.GetHeader("Range") if rangeHeader != "" { - if checkIfRange(ctx, modtime) == condFalse { + if checkIfRange(ctx, etagEmptyOrStrongMatch, modtime) == condFalse { rangeHeader = "" } } return false, rangeHeader } +func checkIfUnmodifiedSince(ctx context.Context, modtime time.Time) condResult { + ius := ctx.GetHeader("If-Unmodified-Since") + if ius == "" || context.IsZeroTime(modtime) { + return condNone + } + if t, err := context.ParseTime(ctx, ius); err == nil { + // The Date-Modified header truncates sub-second precision, so + // use mtime < t+1s instead of mtime <= t to check for unmodified. + if modtime.Before(t.Add(1 * time.Second)) { + return condTrue + } + return condFalse + } + return condNone +} + +func checkIfRange(ctx context.Context, etagEmptyOrStrongMatch func(ifRangeValue string, etagValue string) bool, modtime time.Time) condResult { + if ctx.Method() != http.MethodGet { + return condNone + } + ir := ctx.GetHeader("If-Range") + if ir == "" { + return condNone + } + + if etagEmptyOrStrongMatch(ir, ctx.GetHeader("Etag")) { + return condTrue + } + + // The If-Range value is typically the ETag value, but it may also be + // the modtime date. See golang.org/issue/8367. + if modtime.IsZero() { + return condFalse + } + t, err := context.ParseTime(ctx, ir) + if err != nil { + return condFalse + } + if t.Unix() == modtime.Unix() { + return condTrue + } + return condFalse +} + // name is '/'-separated, not filepath.Separator. func serveFile(ctx context.Context, fs http.FileSystem, name string, redirect bool, showList bool, gzip bool) (string, int) { const indexPage = "/index.html" @@ -826,8 +785,8 @@ func serveFile(ctx context.Context, fs http.FileSystem, name string, redirect bo if !showList { return "", http.StatusForbidden } - if checkIfModifiedSince(ctx, d.ModTime()) == condFalse { - writeNotModified(ctx) + if modified, err := context.CheckIfModifiedSince(ctx, d.ModTime()); !modified && err == nil { + context.WriteNotModified(ctx) return "", http.StatusNotModified } ctx.Header("Last-Modified", d.ModTime().UTC().Format(ctx.Application().ConfigurationReadOnly().GetTimeFormat())) @@ -842,7 +801,7 @@ func serveFile(ctx context.Context, fs http.FileSystem, name string, redirect bo } // else, set the last modified as "serveContent" does. - setLastModified(ctx, d.ModTime()) + context.SetLastModified(ctx, d.ModTime()) // write the file to the response writer. contents, err := ioutil.ReadAll(f) diff --git a/core/router/party.go b/core/router/party.go index 9f840539..a8145f91 100644 --- a/core/router/party.go +++ b/core/router/party.go @@ -211,15 +211,19 @@ type Party interface { // Returns the GET *Route. StaticWeb(requestPath string, systemPath string) *Route - // Layout oerrides the parent template layout with a more specific layout for this Party - // returns this Party, to continue as normal + // Layout overrides the parent template layout with a more specific layout for this Party. + // It returns the current Party. + // + // The "tmplLayoutFile" should be a relative path to the templates dir. // Usage: + // // app := iris.New() + // app.RegisterView(iris.$VIEW_ENGINE("./views", ".$extension")) // my := app.Party("/my").Layout("layouts/mylayout.html") - // { - // my.Get("/", func(ctx context.Context) { - // ctx.MustRender("page1.html", nil) - // }) - // } + // my.Get("/", func(ctx iris.Context) { + // ctx.View("page1.html") + // }) + // + // Examples: https://github.com/kataras/iris/tree/master/_examples/view Layout(tmplLayoutFile string) Party } diff --git a/hero/func_result_test.go b/hero/func_result_test.go index 54a62c62..6c602b65 100644 --- a/hero/func_result_test.go +++ b/hero/func_result_test.go @@ -81,6 +81,22 @@ func GetCustomStructWithError(ctx iris.Context) (s testCustomStruct, err error) return } +type err struct { + Status int `json:"status_code"` + Message string `json:"message"` +} + +func (e err) Dispatch(ctx iris.Context) { + // write the status code based on the err's StatusCode. + ctx.StatusCode(e.Status) + // send to the client the whole object as json + ctx.JSON(e) +} + +func GetCustomErrorAsDispatcher() err { + return err{iris.StatusBadRequest, "this is my error as json"} +} + func TestFuncResult(t *testing.T) { app := iris.New() h := New() @@ -102,6 +118,7 @@ func TestFuncResult(t *testing.T) { app.Get("/custom/struct/with/status/not/ok", h.Handler(GetCustomStructWithStatusNotOk)) app.Get("/custom/struct/with/content/type", h.Handler(GetCustomStructWithContentType)) app.Get("/custom/struct/with/error", h.Handler(GetCustomStructWithError)) + app.Get("/custom/error/as/dispatcher", h.Handler(GetCustomErrorAsDispatcher)) e := httptest.New(t, app) @@ -149,4 +166,10 @@ func TestFuncResult(t *testing.T) { // the content should be not JSON it should be the status code's text // it will fire the error's text Body().Equal("omit return of testCustomStruct and fire error") + + e.GET("/custom/error/as/dispatcher").Expect(). + Status(iris.StatusBadRequest). // the default status code if error is not nil + // the content should be not JSON it should be the status code's text + // it will fire the error's text + JSON().Equal(err{iris.StatusBadRequest, "this is my error as json"}) } diff --git a/httptest/httptest.go b/httptest/httptest.go index 91b85258..59c59cc2 100644 --- a/httptest/httptest.go +++ b/httptest/httptest.go @@ -87,7 +87,12 @@ func New(t *testing.T, app *iris.Application, setters ...OptionSetter) *httpexpe // set the logger or disable it (default) and disable the updater (for any case). app.Configure(iris.WithoutVersionChecker) app.Logger().SetLevel(conf.LogLevel) - app.Build() + if err := app.Build(); err != nil { + if conf.Debug && (conf.LogLevel == "disable" || conf.LogLevel == "disabled") { + app.Logger().Println(err.Error()) + return nil + } + } testConfiguration := httpexpect.Config{ BaseURL: conf.URL, From 981322cfd24f85143185cb220350359395c02aef Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Thu, 25 Jan 2018 06:31:05 +0200 Subject: [PATCH 08/17] no need to default the Configuration.RemoteAddrHeaders to a list of 'dissalowed' header names, the `ctx.RemoteAddr()` made unnecessary checks if no X-header was passed, even if they defaulted to false, this will not give a crazy improvement but it's a good practise Former-commit-id: ba9ed1475a76489df16cac0ed87275b5604f2ad0 --- Dockerfile.build | 2 +- README.md | 2 +- README_GR.md | 2 +- README_RU.md | 2 +- README_ZH.md | 2 +- _examples/subdomains/www/main_test.go | 2 +- configuration.go | 70 +++++++++++++++++---------- context/context.go | 5 +- 8 files changed, 54 insertions(+), 33 deletions(-) diff --git a/Dockerfile.build b/Dockerfile.build index 38d0be83..6c127b0e 100644 --- a/Dockerfile.build +++ b/Dockerfile.build @@ -1,4 +1,4 @@ -FROM golang:1.9-alpine +FROM golang:1.9.3-alpine RUN apk update && apk upgrade && apk add --no-cache bash git RUN go get github.com/iris-contrib/cloud-native-go diff --git a/README.md b/README.md index fdaf6181..52bdccea 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ -[![build status](https://img.shields.io/travis/kataras/iris/master.svg?style=flat-square)](https://travis-ci.org/kataras/iris) [![report card](https://img.shields.io/badge/report%20card-a%2B-ff3333.svg?style=flat-square)](http://goreportcard.com/report/kataras/iris) [![vscode-iris](https://img.shields.io/badge/ext%20-vscode-0c77e3.svg?style=flat-square)](https://github.com/kataras/vscode-iris) [![chat](https://img.shields.io/badge/community-%20chat-00BCD4.svg?style=flat-square)](https://kataras.rocket.chat/channel/iris) [![view examples](https://img.shields.io/badge/learn%20by-examples-0077b3.svg?style=flat-square)](_examples/) [![release](https://img.shields.io/badge/release%20-v10.0-0077b3.svg?style=flat-square)](https://github.com/kataras/iris/releases) +[![build status](https://img.shields.io/travis/kataras/iris/master.svg?style=flat-square)](https://travis-ci.org/kataras/iris) [![report card](https://img.shields.io/badge/report%20card-a%2B-ff3333.svg?style=flat-square)](http://goreportcard.com/report/kataras/iris) [![vscode-iris](https://img.shields.io/badge/ext%20-vscode-0c77e3.svg?style=flat-square)](https://marketplace.visualstudio.com/items?itemName=kataras2006.iris) [![chat](https://img.shields.io/badge/community-%20chat-00BCD4.svg?style=flat-square)](https://kataras.rocket.chat/channel/iris) [![view examples](https://img.shields.io/badge/learn%20by-examples-0077b3.svg?style=flat-square)](_examples/) [![release](https://img.shields.io/badge/release%20-v10.0-0077b3.svg?style=flat-square)](https://github.com/kataras/iris/releases) Iris is a fast, simple yet fully featured and very efficient web framework for Go. diff --git a/README_GR.md b/README_GR.md index 8eb17c9b..fae98ae6 100644 --- a/README_GR.md +++ b/README_GR.md @@ -2,7 +2,7 @@ -[![build status](https://img.shields.io/travis/kataras/iris/master.svg?style=flat-square)](https://travis-ci.org/kataras/iris) [![report card](https://img.shields.io/badge/report%20card-a%2B-ff3333.svg?style=flat-square)](http://goreportcard.com/report/kataras/iris) [![vscode-iris](https://img.shields.io/badge/ext%20-vscode-0c77e3.svg?style=flat-square)](https://github.com/kataras/vscode-iris) [![chat](https://img.shields.io/badge/community-%20chat-00BCD4.svg?style=flat-square)](https://kataras.rocket.chat/channel/iris) [![view examples](https://img.shields.io/badge/learn%20by-examples-0077b3.svg?style=flat-square)](_examples/) [![release](https://img.shields.io/badge/release%20-v10.0-0077b3.svg?style=flat-square)](https://github.com/kataras/iris/releases) +[![build status](https://img.shields.io/travis/kataras/iris/master.svg?style=flat-square)](https://travis-ci.org/kataras/iris) [![report card](https://img.shields.io/badge/report%20card-a%2B-ff3333.svg?style=flat-square)](http://goreportcard.com/report/kataras/iris) [![vscode-iris](https://img.shields.io/badge/ext%20-vscode-0c77e3.svg?style=flat-square)](https://marketplace.visualstudio.com/items?itemName=kataras2006.iris) [![chat](https://img.shields.io/badge/community-%20chat-00BCD4.svg?style=flat-square)](https://kataras.rocket.chat/channel/iris) [![view examples](https://img.shields.io/badge/learn%20by-examples-0077b3.svg?style=flat-square)](_examples/) [![release](https://img.shields.io/badge/release%20-v10.0-0077b3.svg?style=flat-square)](https://github.com/kataras/iris/releases) Το Iris είναι ένα γρήγορο, απλό αλλά και πλήρως λειτουργικό και πολύ αποδοτικό web framework για τη Go. diff --git a/README_RU.md b/README_RU.md index 520cf0ca..f898d595 100644 --- a/README_RU.md +++ b/README_RU.md @@ -2,7 +2,7 @@ -[![build status](https://img.shields.io/travis/kataras/iris/master.svg?style=flat-square)](https://travis-ci.org/kataras/iris) [![report card](https://img.shields.io/badge/report%20card-a%2B-ff3333.svg?style=flat-square)](http://goreportcard.com/report/kataras/iris) [![vscode-iris](https://img.shields.io/badge/ext%20-vscode-0c77e3.svg?style=flat-square)](https://github.com/kataras/vscode-iris) [![chat](https://img.shields.io/badge/community-%20chat-00BCD4.svg?style=flat-square)](https://kataras.rocket.chat/channel/iris) [![view examples](https://img.shields.io/badge/learn%20by-examples-0077b3.svg?style=flat-square)](_examples/) [![release](https://img.shields.io/badge/release%20-v10.0-0077b3.svg?style=flat-square)](https://github.com/kataras/iris/releases) +[![build status](https://img.shields.io/travis/kataras/iris/master.svg?style=flat-square)](https://travis-ci.org/kataras/iris) [![report card](https://img.shields.io/badge/report%20card-a%2B-ff3333.svg?style=flat-square)](http://goreportcard.com/report/kataras/iris) [![vscode-iris](https://img.shields.io/badge/ext%20-vscode-0c77e3.svg?style=flat-square)](https://marketplace.visualstudio.com/items?itemName=kataras2006.iris) [![chat](https://img.shields.io/badge/community-%20chat-00BCD4.svg?style=flat-square)](https://kataras.rocket.chat/channel/iris) [![view examples](https://img.shields.io/badge/learn%20by-examples-0077b3.svg?style=flat-square)](_examples/) [![release](https://img.shields.io/badge/release%20-v10.0-0077b3.svg?style=flat-square)](https://github.com/kataras/iris/releases) Iris - это быстрая, простая, но полнофункциональная и очень эффективная веб-платформа для Go. diff --git a/README_ZH.md b/README_ZH.md index bd58ada6..aef23a95 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -2,7 +2,7 @@ -[![build status](https://img.shields.io/travis/kataras/iris/master.svg?style=flat-square)](https://travis-ci.org/kataras/iris) [![report card](https://img.shields.io/badge/report%20card-a%2B-ff3333.svg?style=flat-square)](http://goreportcard.com/report/kataras/iris) [![vscode-iris](https://img.shields.io/badge/ext%20-vscode-0c77e3.svg?style=flat-square)](https://github.com/kataras/vscode-iris) [![chat](https://img.shields.io/badge/community-%20chat-00BCD4.svg?style=flat-square)](https://kataras.rocket.chat/channel/iris) [![view examples](https://img.shields.io/badge/learn%20by-examples-0077b3.svg?style=flat-square)](_examples/) [![release](https://img.shields.io/badge/release%20-v10.0-0077b3.svg?style=flat-square)](https://github.com/kataras/iris/releases) +[![build status](https://img.shields.io/travis/kataras/iris/master.svg?style=flat-square)](https://travis-ci.org/kataras/iris) [![report card](https://img.shields.io/badge/report%20card-a%2B-ff3333.svg?style=flat-square)](http://goreportcard.com/report/kataras/iris) [![vscode-iris](https://img.shields.io/badge/ext%20-vscode-0c77e3.svg?style=flat-square)](https://marketplace.visualstudio.com/items?itemName=kataras2006.iris) [![chat](https://img.shields.io/badge/community-%20chat-00BCD4.svg?style=flat-square)](https://kataras.rocket.chat/channel/iris) [![view examples](https://img.shields.io/badge/learn%20by-examples-0077b3.svg?style=flat-square)](_examples/) [![release](https://img.shields.io/badge/release%20-v10.0-0077b3.svg?style=flat-square)](https://github.com/kataras/iris/releases) Iris 是一款超快、简洁高效的 Go 语言 Web开发框架。 diff --git a/_examples/subdomains/www/main_test.go b/_examples/subdomains/www/main_test.go index cd698384..db6c9421 100644 --- a/_examples/subdomains/www/main_test.go +++ b/_examples/subdomains/www/main_test.go @@ -47,7 +47,7 @@ func TestSubdomainWWW(t *testing.T) { req := e.Request(test.method, test.path) if subdomain := test.subdomain; subdomain != "" { - req = req.WithURL("http://" + subdomain + "." + host) + req.WithURL("http://" + subdomain + "." + host) } req.Expect(). diff --git a/configuration.go b/configuration.go index 4fb3f0ec..2d8e8cde 100644 --- a/configuration.go +++ b/configuration.go @@ -330,10 +330,16 @@ func WithPostMaxMemory(limit int64) Configurator { // WithRemoteAddrHeader enables or adds a new or existing request header name // that can be used to validate the client's real IP. // -// Existing values are: -// "X-Real-Ip": false, -// "X-Forwarded-For": false, -// "CF-Connecting-IP": false +// By-default no "X-" header is consired safe to be used for retrieving the +// client's IP address, because those headers can manually change by +// the client. But sometimes are useful e.g., when behind a proxy +// you want to enable the "X-Forwarded-For" or when cloudflare +// you want to enable the "CF-Connecting-IP", inneed you +// can allow the `ctx.RemoteAddr()` to use any header +// that the client may sent. +// +// Defaults to an empty map but an example usage is: +// WithRemoteAddrHeader("X-Forwarded-For") // // Look `context.RemoteAddr()` for more. func WithRemoteAddrHeader(headerName string) Configurator { @@ -346,12 +352,12 @@ func WithRemoteAddrHeader(headerName string) Configurator { } // WithoutRemoteAddrHeader disables an existing request header name -// that can be used to validate the client's real IP. +// that can be used to validate and parse the client's real IP. // -// Existing values are: -// "X-Real-Ip": false, -// "X-Forwarded-For": false, -// "CF-Connecting-IP": false +// +// Keep note that RemoteAddrHeaders is already defaults to an empty map +// so you don't have to call this Configurator if you didn't +// add allowed headers via configuration or via `WithRemoteAddrHeader` before. // // Look `context.RemoteAddr()` for more. func WithoutRemoteAddrHeader(headerName string) Configurator { @@ -511,13 +517,22 @@ type Configuration struct { // // Defaults to "iris.viewData" ViewDataContextKey string `json:"viewDataContextKey,omitempty" yaml:"ViewDataContextKey" toml:"ViewDataContextKey"` - // RemoteAddrHeaders returns the allowed request headers names + // RemoteAddrHeaders are the allowed request headers names // that can be valid to parse the client's IP based on. + // By-default no "X-" header is consired safe to be used for retrieving the + // client's IP address, because those headers can manually change by + // the client. But sometimes are useful e.g., when behind a proxy + // you want to enable the "X-Forwarded-For" or when cloudflare + // you want to enable the "CF-Connecting-IP", inneed you + // can allow the `ctx.RemoteAddr()` to use any header + // that the client may sent. // - // Defaults to: - // "X-Real-Ip": false, - // "X-Forwarded-For": false, - // "CF-Connecting-IP": false + // Defaults to an empty map but an example usage is: + // RemoteAddrHeaders { + // "X-Real-Ip": true, + // "X-Forwarded-For": true, + // "CF-Connecting-IP": true, + // } // // Look `context.RemoteAddr()` for more. RemoteAddrHeaders map[string]bool `json:"remoteAddrHeaders,omitempty" yaml:"RemoteAddrHeaders" toml:"RemoteAddrHeaders"` @@ -637,11 +652,20 @@ func (c Configuration) GetViewDataContextKey() string { // GetRemoteAddrHeaders returns the allowed request headers names // that can be valid to parse the client's IP based on. +// By-default no "X-" header is consired safe to be used for retrieving the +// client's IP address, because those headers can manually change by +// the client. But sometimes are useful e.g., when behind a proxy +// you want to enable the "X-Forwarded-For" or when cloudflare +// you want to enable the "CF-Connecting-IP", inneed you +// can allow the `ctx.RemoteAddr()` to use any header +// that the client may sent. // -// Defaults to: -// "X-Real-Ip": false, -// "X-Forwarded-For": false, -// "CF-Connecting-IP": false +// Defaults to an empty map but an example usage is: +// RemoteAddrHeaders { +// "X-Real-Ip": true, +// "X-Forwarded-For": true, +// "CF-Connecting-IP": true, +// } // // Look `context.RemoteAddr()` for more. func (c Configuration) GetRemoteAddrHeaders() map[string]bool { @@ -777,12 +801,8 @@ func DefaultConfiguration() Configuration { TranslateLanguageContextKey: "iris.language", ViewLayoutContextKey: "iris.viewLayout", ViewDataContextKey: "iris.viewData", - RemoteAddrHeaders: map[string]bool{ - "X-Real-Ip": false, - "X-Forwarded-For": false, - "CF-Connecting-IP": false, - }, - EnableOptimizations: false, - Other: make(map[string]interface{}), + RemoteAddrHeaders: make(map[string]bool), + EnableOptimizations: false, + Other: make(map[string]interface{}), } } diff --git a/context/context.go b/context/context.go index ed58bda7..494b18e5 100644 --- a/context/context.go +++ b/context/context.go @@ -1366,6 +1366,8 @@ func (ctx *context) IsWWW() bool { return false } +const xForwardedForHeaderKey = "X-Forwarded-For" + // RemoteAddr tries to parse and return the real client's request IP. // // Based on allowed headers names that can be modified from Configuration.RemoteAddrHeaders. @@ -1377,14 +1379,13 @@ func (ctx *context) IsWWW() bool { // `Configuration.WithRemoteAddrHeader(...)`, // `Configuration.WithoutRemoteAddrHeader(...)` for more. func (ctx *context) RemoteAddr() string { - remoteHeaders := ctx.Application().ConfigurationReadOnly().GetRemoteAddrHeaders() for headerName, enabled := range remoteHeaders { if enabled { headerValue := ctx.GetHeader(headerName) // exception needed for 'X-Forwarded-For' only , if enabled. - if headerName == "X-Forwarded-For" { + if headerName == xForwardedForHeaderKey { idx := strings.IndexByte(headerValue, ',') if idx >= 0 { headerValue = headerValue[0:idx] From befb1f0c08c3d290a5df2c8d2ef4a12ad870064b Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Thu, 25 Jan 2018 15:09:24 +0200 Subject: [PATCH 09/17] some major improvements to the (server-side) cache middleware and an example of a client-side responsibility cache Former-commit-id: 93d3a7a6f163c6d49f315f86d10e63f7b1b1d93a --- _examples/cache/client-side/main.go | 27 +++++++ cache/client/handler.go | 21 ++++-- cache/client/response_recorder.go | 109 ---------------------------- cache/client/utils.go | 20 ----- cache/entry/entry.go | 31 +++++++- cache/entry/response.go | 31 +++++--- core/router/fs.go | 2 +- 7 files changed, 90 insertions(+), 151 deletions(-) create mode 100644 _examples/cache/client-side/main.go delete mode 100644 cache/client/response_recorder.go delete mode 100644 cache/client/utils.go diff --git a/_examples/cache/client-side/main.go b/_examples/cache/client-side/main.go new file mode 100644 index 00000000..02b054f3 --- /dev/null +++ b/_examples/cache/client-side/main.go @@ -0,0 +1,27 @@ +// Package main shows how you can use the `WriteWithExpiration` +// based on the "modtime", if it's newer than the request header then +// it will refresh the contents, otherwise will let the client (99.9% the browser) +// to handle the cache mechanism, it's faster than iris.Cache because server-side +// has nothing to do and no need to store the responses in the memory. +package main + +import ( + "fmt" + "time" + + "github.com/kataras/iris" +) + +var modtime = time.Now() + +func greet(ctx iris.Context) { + ctx.Header("X-Custom", "my custom header") + response := fmt.Sprintf("Hello World! %s", time.Now()) + ctx.WriteWithExpiration([]byte(response), modtime) +} + +func main() { + app := iris.New() + app.Get("/", greet) + app.Run(iris.Addr(":8080")) +} diff --git a/cache/client/handler.go b/cache/client/handler.go index 389d0336..2210f49b 100644 --- a/cache/client/handler.go +++ b/cache/client/handler.go @@ -4,7 +4,6 @@ import ( "sync" "time" - "github.com/kataras/iris/cache/cfg" "github.com/kataras/iris/cache/client/rule" "github.com/kataras/iris/cache/entry" "github.com/kataras/iris/context" @@ -66,6 +65,12 @@ var emptyHandler = func(ctx context.Context) { ctx.StopExecution() } +func parseLifeChanger(ctx context.Context) entry.LifeChanger { + return func() time.Duration { + return time.Duration(ctx.MaxAge()) * time.Second + } +} + ///TODO: debug this and re-run the parallel tests on larger scale, // because I think we have a bug here when `core/router#StaticWeb` is used after this middleware. func (h *Handler) ServeHTTP(ctx context.Context) { @@ -135,14 +140,19 @@ func (h *Handler) ServeHTTP(ctx context.Context) { // no need to copy the body, its already done inside body := recorder.Body() if len(body) == 0 { - // if no body then just exit + // if no body then just exit. return } // check for an expiration time if the // given expiration was not valid then check for GetMaxAge & // update the response & release the recorder - e.Reset(recorder.StatusCode(), recorder.Header().Get(cfg.ContentTypeHeader), body, GetMaxAge(ctx.Request())) + e.Reset( + recorder.StatusCode(), + recorder.Header(), + body, + parseLifeChanger(ctx), + ) // fmt.Printf("reset cache entry\n") // fmt.Printf("key: %s\n", key) @@ -152,12 +162,13 @@ func (h *Handler) ServeHTTP(ctx context.Context) { } // if it's valid then just write the cached results - ctx.ContentType(response.ContentType()) + entry.CopyHeaders(ctx.ResponseWriter().Header(), response.Headers()) + context.SetLastModified(ctx, e.LastModified) ctx.StatusCode(response.StatusCode()) ctx.Write(response.Body()) // fmt.Printf("key: %s\n", key) - // fmt.Printf("write content type: %s\n", response.ContentType()) + // fmt.Printf("write content type: %s\n", response.Headers()["ContentType"]) // fmt.Printf("write body len: %d\n", len(response.Body())) } diff --git a/cache/client/response_recorder.go b/cache/client/response_recorder.go deleted file mode 100644 index 04dd1a7d..00000000 --- a/cache/client/response_recorder.go +++ /dev/null @@ -1,109 +0,0 @@ -package client - -import ( - "net/http" - "sync" -) - -var rpool = sync.Pool{} - -// AcquireResponseRecorder returns a ResponseRecorder -func AcquireResponseRecorder(underline http.ResponseWriter) *ResponseRecorder { - v := rpool.Get() - var res *ResponseRecorder - if v != nil { - res = v.(*ResponseRecorder) - } else { - res = &ResponseRecorder{} - } - res.underline = underline - return res -} - -// ReleaseResponseRecorder releases a ResponseRecorder which has been previously received by AcquireResponseRecorder -func ReleaseResponseRecorder(res *ResponseRecorder) { - res.underline = nil - res.statusCode = 0 - res.chunks = res.chunks[0:0] - rpool.Put(res) -} - -// ResponseRecorder is used by httpcache to be able to get the Body and the StatusCode of a request handler -type ResponseRecorder struct { - underline http.ResponseWriter - chunks [][]byte // 2d because .Write can be called more than one time in the same handler and we want to cache all of them - statusCode int // the saved status code which will be used from the cache service -} - -// Body joins the chunks to one []byte slice, this is the full body -func (res *ResponseRecorder) Body() []byte { - var body []byte - for i := range res.chunks { - body = append(body, res.chunks[i]...) - } - return body -} - -// ContentType returns the header's value of "Content-Type" -func (res *ResponseRecorder) ContentType() string { - return res.Header().Get("Content-Type") -} - -// StatusCode returns the status code, if not given then returns 200 -// but doesn't changes the existing behavior -func (res *ResponseRecorder) StatusCode() int { - if res.statusCode == 0 { - return 200 - } - return res.statusCode -} - -// Header returns the header map that will be sent by -// WriteHeader. Changing the header after a call to -// WriteHeader (or Write) has no effect unless the modified -// headers were declared as trailers by setting the -// "Trailer" header before the call to WriteHeader (see example). -// To suppress implicit response headers, set their value to nil. -func (res *ResponseRecorder) Header() http.Header { - return res.underline.Header() -} - -// Write writes the data to the connection as part of an HTTP reply. -// -// If WriteHeader has not yet been called, Write calls -// WriteHeader(http.StatusOK) before writing the data. If the Header -// does not contain a Content-Type line, Write adds a Content-Type set -// to the result of passing the initial 512 bytes of written data to -// DetectContentType. -// -// Depending on the HTTP protocol version and the client, calling -// Write or WriteHeader may prevent future reads on the -// Request.Body. For HTTP/1.x requests, handlers should read any -// needed request body data before writing the response. Once the -// headers have been flushed (due to either an explicit Flusher.Flush -// call or writing enough data to trigger a flush), the request body -// may be unavailable. For HTTP/2 requests, the Go HTTP server permits -// handlers to continue to read the request body while concurrently -// writing the response. However, such behavior may not be supported -// by all HTTP/2 clients. Handlers should read before writing if -// possible to maximize compatibility. -func (res *ResponseRecorder) Write(contents []byte) (int, error) { - if res.statusCode == 0 { // if not setted set it here - res.WriteHeader(http.StatusOK) - } - res.chunks = append(res.chunks, contents) - return res.underline.Write(contents) -} - -// WriteHeader sends an HTTP response header with status code. -// If WriteHeader is not called explicitly, the first call to Write -// will trigger an implicit WriteHeader(http.StatusOK). -// Thus explicit calls to WriteHeader are mainly used to -// send error codes. -func (res *ResponseRecorder) WriteHeader(statusCode int) { - if res.statusCode == 0 { // set it only if not setted already, we don't want logs about multiple sends - res.statusCode = statusCode - res.underline.WriteHeader(statusCode) - } - -} diff --git a/cache/client/utils.go b/cache/client/utils.go deleted file mode 100644 index 33e4bd8c..00000000 --- a/cache/client/utils.go +++ /dev/null @@ -1,20 +0,0 @@ -package client - -import ( - "net/http" - "time" - - "github.com/kataras/iris/cache/entry" -) - -// GetMaxAge parses the "Cache-Control" header -// and returns a LifeChanger which can be passed -// to the response's Reset -func GetMaxAge(r *http.Request) entry.LifeChanger { - return func() time.Duration { - cacheControlHeader := r.Header.Get("Cache-Control") - // headerCacheDur returns the seconds - headerCacheDur := entry.ParseMaxAge(cacheControlHeader) - return time.Duration(headerCacheDur) * time.Second - } -} diff --git a/cache/entry/entry.go b/cache/entry/entry.go index 8e7cbd9b..141d1ede 100644 --- a/cache/entry/entry.go +++ b/cache/entry/entry.go @@ -13,6 +13,11 @@ type Entry struct { // ExpiresAt is the time which this cache will not be available expiresAt time.Time + // when `Reset` this value is reseting to time.Now(), + // it's used to send the "Last-Modified" header, + // some clients may need it. + LastModified time.Time + // Response the response should be served to the client response *Response // but we need the key to invalidate manually...xmm @@ -78,10 +83,23 @@ func (e *Entry) ChangeLifetime(fdur LifeChanger) { } } +// CopyHeaders clones headers "src" to "dst" . +func CopyHeaders(dst map[string][]string, src map[string][]string) { + if dst == nil || src == nil { + return + } + + for k, vv := range src { + v := make([]string, len(vv)) + copy(v, vv) + dst[k] = v + } +} + // Reset called each time the entry is expired // and the handler calls this after the original handler executed // to re-set the response with the new handler's content result -func (e *Entry) Reset(statusCode int, contentType string, +func (e *Entry) Reset(statusCode int, headers map[string][]string, body []byte, lifeChanger LifeChanger) { if e.response == nil { @@ -91,8 +109,10 @@ func (e *Entry) Reset(statusCode int, contentType string, e.response.statusCode = statusCode } - if contentType != "" { - e.response.contentType = contentType + if len(headers) > 0 { + newHeaders := make(map[string][]string, len(headers)) + CopyHeaders(newHeaders, headers) + e.response.headers = newHeaders } e.response.body = body @@ -101,5 +121,8 @@ func (e *Entry) Reset(statusCode int, contentType string, if lifeChanger != nil { e.ChangeLifetime(lifeChanger) } - e.expiresAt = time.Now().Add(e.life) + + now := time.Now() + e.expiresAt = now.Add(e.life) + e.LastModified = now } diff --git a/cache/entry/response.go b/cache/entry/response.go index 53569c59..7e24f44b 100644 --- a/cache/entry/response.go +++ b/cache/entry/response.go @@ -1,19 +1,21 @@ package entry +import "net/http" + // Response is the cached response will be send to the clients // its fields setted at runtime on each of the non-cached executions // non-cached executions = first execution, and each time after -// cache expiration datetime passed +// cache expiration datetime passed. type Response struct { - // statusCode for the response cache handler + // statusCode for the response cache handler. statusCode int - // contentType for the response cache handler - contentType string - // body is the contents will be served by the cache handler + // body is the contents will be served by the cache handler. body []byte + // the total headers of the response, including content type. + headers http.Header } -// StatusCode returns a valid status code +// StatusCode returns a valid status code. func (r *Response) StatusCode() int { if r.statusCode <= 0 { r.statusCode = 200 @@ -22,14 +24,19 @@ func (r *Response) StatusCode() int { } // ContentType returns a valid content type -func (r *Response) ContentType() string { - if r.contentType == "" { - r.contentType = "text/html; charset=utf-8" - } - return r.contentType +// func (r *Response) ContentType() string { +// if r.headers == "" { +// r.contentType = "text/html; charset=utf-8" +// } +// return r.contentType +// } + +// Headers returns the total headers of the cached response. +func (r *Response) Headers() http.Header { + return r.headers } -// Body returns contents will be served by the cache handler +// Body returns contents will be served by the cache handler. func (r *Response) Body() []byte { return r.body } diff --git a/core/router/fs.go b/core/router/fs.go index 822058cf..6c9ac7c8 100644 --- a/core/router/fs.go +++ b/core/router/fs.go @@ -823,7 +823,7 @@ func serveFile(ctx context.Context, fs http.FileSystem, name string, redirect bo // and the binary data inside "f". detectOrWriteContentType(ctx, d.Name(), f) - return "", 200 + return "", http.StatusOK } // toHTTPError returns a non-specific HTTP error message and status code From 969c2e87d4c62fdb99d5586c47441e990f51f1fb Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Thu, 25 Jan 2018 16:19:45 +0200 Subject: [PATCH 10/17] add `Cache304` as an alternative to the server-side kataras/iris/cache middleware - it can perform better with less server overheat but it comes with a cost of 304 instead of 200 so custom clients must make that check Former-commit-id: b0ba68c528c870fe060e2825c35689771a1d3680 --- _examples/README.md | 1 + _examples/cache/client-side/main.go | 28 +++++++--- _examples/cache/simple/main.go | 5 ++ cache/cache.go | 11 +++- cache/client/handler.go | 2 +- context/context.go | 84 +++++++++++++++++++++++++---- core/router/fs.go | 16 +++--- iris.go | 23 +++++++- 8 files changed, 140 insertions(+), 30 deletions(-) diff --git a/_examples/README.md b/_examples/README.md index 3076b4c5..2ba8c680 100644 --- a/_examples/README.md +++ b/_examples/README.md @@ -382,6 +382,7 @@ The `httptest` package is your way for end-to-end HTTP testing, it uses the http iris cache library lives on its own [package](https://github.com/kataras/iris/tree/master/cache). - [Simple](cache/simple/main.go) +- [Client-Side (304)](cache/client-side/main.go) - part of the iris context core > You're free to use your own favourite caching package if you'd like so. diff --git a/_examples/cache/client-side/main.go b/_examples/cache/client-side/main.go index 02b054f3..8b21438e 100644 --- a/_examples/cache/client-side/main.go +++ b/_examples/cache/client-side/main.go @@ -6,22 +6,34 @@ package main import ( - "fmt" "time" "github.com/kataras/iris" ) -var modtime = time.Now() - -func greet(ctx iris.Context) { - ctx.Header("X-Custom", "my custom header") - response := fmt.Sprintf("Hello World! %s", time.Now()) - ctx.WriteWithExpiration([]byte(response), modtime) -} +const refreshEvery = 10 * time.Second func main() { app := iris.New() + app.Use(iris.Cache304(refreshEvery)) + // same as: + // app.Use(func(ctx iris.Context) { + // now := time.Now() + // if modified, err := ctx.CheckIfModifiedSince(now.Add(-refresh)); !modified && err == nil { + // ctx.WriteNotModified() + // return + // } + + // ctx.SetLastModified(now) + + // ctx.Next() + // }) + app.Get("/", greet) app.Run(iris.Addr(":8080")) } + +func greet(ctx iris.Context) { + ctx.Header("X-Custom", "my custom header") + ctx.Writef("Hello World! %s", time.Now()) +} diff --git a/_examples/cache/simple/main.go b/_examples/cache/simple/main.go index 04aa0246..09f88881 100644 --- a/_examples/cache/simple/main.go +++ b/_examples/cache/simple/main.go @@ -73,3 +73,8 @@ func writeMarkdown(ctx iris.Context) { ctx.Markdown(markdownContents) } + +/* Note that `StaticWeb` does use the browser's disk caching by-default +therefore, register the cache handler AFTER any StaticWeb calls, +for a faster solution that server doesn't need to keep track of the response +navigate to https://github.com/kataras/iris/blob/master/_examples/cache/client-side/main.go */ diff --git a/cache/cache.go b/cache/cache.go index 09d5ab91..c6d100da 100644 --- a/cache/cache.go +++ b/cache/cache.go @@ -1,4 +1,7 @@ -/* Package cache provides cache capabilities with rich support of options and rules. +/* Package cache provides server-side caching capabilities with rich support of options and rules. + +Use it for server-side caching, see the `iris#Cache304` for an alternative approach that +may fit your needs most. Example code: @@ -37,6 +40,9 @@ import ( // // All types of response can be cached, templates, json, text, anything. // +// Use it for server-side caching, see the `iris#Cache304` for an alternative approach that +// may fit your needs most. +// // You can add validators with this function. func Cache(expiration time.Duration) *client.Handler { return client.NewHandler(expiration) @@ -49,6 +55,9 @@ func Cache(expiration time.Duration) *client.Handler { // // All types of response can be cached, templates, json, text, anything. // +// Use it for server-side caching, see the `iris#Cache304` for an alternative approach that +// may fit your needs most. +// // it returns a context.Handler which can be used as a middleware, for more options use the `Cache`. // // Examples can be found at: https://github.com/kataras/iris/tree/master/_examples/#caching diff --git a/cache/client/handler.go b/cache/client/handler.go index 2210f49b..6cc35345 100644 --- a/cache/client/handler.go +++ b/cache/client/handler.go @@ -163,7 +163,7 @@ func (h *Handler) ServeHTTP(ctx context.Context) { // if it's valid then just write the cached results entry.CopyHeaders(ctx.ResponseWriter().Header(), response.Headers()) - context.SetLastModified(ctx, e.LastModified) + ctx.SetLastModified(e.LastModified) ctx.StatusCode(response.StatusCode()) ctx.Write(response.Body()) diff --git a/context/context.go b/context/context.go index 494b18e5..35a91896 100644 --- a/context/context.go +++ b/context/context.go @@ -639,6 +639,39 @@ type Context interface { // // Returns the number of bytes written and any write error encountered. WriteString(body string) (int, error) + + // SetLastModified sets the "Last-Modified" based on the "modtime" input. + // If "modtime" is zero then it does nothing. + // + // It's mostly internally on core/router and context packages. + // + // Note that modtime.UTC() is being used instead of just modtime, so + // you don't have to know the internals in order to make that works. + SetLastModified(modtime time.Time) + // CheckIfModifiedSince checks if the response is modified since the "modtime". + // Note that it has nothing to do with server-side caching. + // It does those checks by checking if the "If-Modified-Since" request header + // sent by client or a previous server response header + // (e.g with WriteWithExpiration or StaticEmbedded or Favicon etc.) + // is a valid one and it's before the "modtime". + // + // A check for !modtime && err == nil is necessary to make sure that + // it's not modified since, because it may return false but without even + // had the chance to check the client-side (request) header due to some errors, + // like the HTTP Method is not "GET" or "HEAD" or if the "modtime" is zero + // or if parsing time from the header failed. + // + // It's mostly used internally, e.g. `context#WriteWithExpiration`. + // + // Note that modtime.UTC() is being used instead of just modtime, so + // you don't have to know the internals in order to make that works. + CheckIfModifiedSince(modtime time.Time) (bool, error) + // WriteNotModified sends a 304 "Not Modified" status code to the client, + // it makes sure that the content type, the content length headers + // and any "ETag" are removed before the response sent. + // + // It's mostly used internally on core/router/fs.go and context methods. + WriteNotModified() // WriteWithExpiration like Write but it sends with an expiration datetime // which is refreshed every package-level `StaticCacheDuration` field. WriteWithExpiration(body []byte, modtime time.Time) (int, error) @@ -913,6 +946,35 @@ var LimitRequestBodySize = func(maxRequestBodySizeBytes int64) Handler { } } +// Cache304 sends a `StatusNotModified` (304) whenever +// the "If-Modified-Since" request header (time) is before the +// time.Now() + expiresEvery (always compared to their UTC values). +// Use this `context#Cache304` instead of the "github.com/kataras/iris/cache" or iris.Cache +// for better performance. +// Clients that are compatible with the http RCF (all browsers are and tools like postman) +// will handle the caching. +// The only disadvantage of using that instead of server-side caching +// is that this method will send a 304 status code instead of 200, +// So, if you use it side by side with other micro services +// you have to check for that status code as well for a valid response. +// +// Developers are free to extend this method's behavior +// by watching system directories changes manually and use of the `ctx.WriteWithExpiration` +// with a "modtime" based on the file modified date, +// simillary to the `StaticWeb`(StaticWeb sends an OK(200) and browser disk caching instead of 304). +var Cache304 = func(expiresEvery time.Duration) Handler { + return func(ctx Context) { + now := time.Now() + if modified, err := ctx.CheckIfModifiedSince(now.Add(-expiresEvery)); !modified && err == nil { + ctx.WriteNotModified() + return + } + + ctx.SetLastModified(now) + ctx.Next() + } +} + // Gzip is a middleware which enables writing // using gzip compression, if client supports. var Gzip = func(ctx Context) { @@ -2092,9 +2154,9 @@ var FormatTime = func(ctx Context, t time.Time) string { // If "modtime" is zero then it does nothing. // // It's mostly internally on core/router and context packages. -func SetLastModified(ctx Context, modtime time.Time) { +func (ctx *context) SetLastModified(modtime time.Time) { if !IsZeroTime(modtime) { - ctx.Header(lastModifiedHeaderKey, FormatTime(ctx, modtime)) // or modtime.UTC()? + ctx.Header(lastModifiedHeaderKey, FormatTime(ctx, modtime.UTC())) // or modtime.UTC()? } } @@ -2112,7 +2174,7 @@ func SetLastModified(ctx Context, modtime time.Time) { // or if parsing time from the header failed. // // It's mostly used internally, e.g. `context#WriteWithExpiration`. -func CheckIfModifiedSince(ctx Context, modtime time.Time) (bool, error) { +func (ctx *context) CheckIfModifiedSince(modtime time.Time) (bool, error) { if method := ctx.Method(); method != http.MethodGet && method != http.MethodHead { return false, errors.New("skip: method") } @@ -2126,7 +2188,7 @@ func CheckIfModifiedSince(ctx Context, modtime time.Time) (bool, error) { } // sub-second precision, so // use mtime < t+1s instead of mtime <= t to check for unmodified. - if modtime.Before(t.Add(1 * time.Second)) { + if modtime.UTC().Before(t.Add(1 * time.Second)) { return false, nil } return true, nil @@ -2137,7 +2199,7 @@ func CheckIfModifiedSince(ctx Context, modtime time.Time) (bool, error) { // and any "ETag" are removed before the response sent. // // It's mostly used internally on core/router/fs.go and context methods. -func WriteNotModified(ctx Context) { +func (ctx *context) WriteNotModified() { // RFC 7232 section 4.1: // a sender SHOULD NOT generate representation metadata other than the // above listed fields unless said metadata exists for the purpose of @@ -2155,12 +2217,12 @@ func WriteNotModified(ctx Context) { // WriteWithExpiration like Write but it sends with an expiration datetime // which is refreshed every package-level `StaticCacheDuration` field. func (ctx *context) WriteWithExpiration(body []byte, modtime time.Time) (int, error) { - if modified, err := CheckIfModifiedSince(ctx, modtime); !modified && err == nil { - WriteNotModified(ctx) + if modified, err := ctx.CheckIfModifiedSince(modtime); !modified && err == nil { + ctx.WriteNotModified() return 0, nil } - SetLastModified(ctx, modtime) + ctx.SetLastModified(modtime) return ctx.writer.Write(body) } @@ -2750,13 +2812,13 @@ const ( // You can define your own "Content-Type" header also, after this function call // Doesn't implements resuming (by range), use ctx.SendFile instead func (ctx *context) ServeContent(content io.ReadSeeker, filename string, modtime time.Time, gzipCompression bool) error { - if modified, err := CheckIfModifiedSince(ctx, modtime); !modified && err == nil { - WriteNotModified(ctx) + if modified, err := ctx.CheckIfModifiedSince(modtime); !modified && err == nil { + ctx.WriteNotModified() return nil } ctx.ContentType(filename) - SetLastModified(ctx, modtime) + ctx.SetLastModified(modtime) var out io.Writer if gzipCompression && ctx.ClientSupportsGzip() { ctx.writer.Header().Add(varyHeaderKey, acceptEncodingHeaderKey) diff --git a/core/router/fs.go b/core/router/fs.go index 6c9ac7c8..ace1a494 100644 --- a/core/router/fs.go +++ b/core/router/fs.go @@ -411,7 +411,7 @@ func detectOrWriteContentType(ctx context.Context, name string, content io.ReadS // content must be seeked to the beginning of the file. // The sizeFunc is called at most once. Its error, if any, is sent in the HTTP response. func serveContent(ctx context.Context, name string, modtime time.Time, sizeFunc func() (int64, error), content io.ReadSeeker) (string, int) /* we could use the TransactionErrResult but prefer not to create new objects for each of the errors on static file handlers*/ { - context.SetLastModified(ctx, modtime) + ctx.SetLastModified(modtime) done, rangeReq := checkPreconditions(ctx, modtime) if done { return "", http.StatusNotModified @@ -651,15 +651,15 @@ func checkPreconditions(ctx context.Context, modtime time.Time) (done bool, rang switch checkIfNoneMatch(ctx) { case condFalse: if ctx.Method() == http.MethodGet || ctx.Method() == http.MethodHead { - context.WriteNotModified(ctx) + ctx.WriteNotModified() return true, "" } ctx.StatusCode(http.StatusPreconditionFailed) return true, "" case condNone: - if modified, err := context.CheckIfModifiedSince(ctx, modtime); !modified && err == nil { - context.WriteNotModified(ctx) + if modified, err := ctx.CheckIfModifiedSince(modtime); !modified && err == nil { + ctx.WriteNotModified() return true, "" } } @@ -785,11 +785,11 @@ func serveFile(ctx context.Context, fs http.FileSystem, name string, redirect bo if !showList { return "", http.StatusForbidden } - if modified, err := context.CheckIfModifiedSince(ctx, d.ModTime()); !modified && err == nil { - context.WriteNotModified(ctx) + if modified, err := ctx.CheckIfModifiedSince(d.ModTime()); !modified && err == nil { + ctx.WriteNotModified() return "", http.StatusNotModified } - ctx.Header("Last-Modified", d.ModTime().UTC().Format(ctx.Application().ConfigurationReadOnly().GetTimeFormat())) + ctx.SetLastModified(d.ModTime()) return dirList(ctx, f) } @@ -801,7 +801,7 @@ func serveFile(ctx context.Context, fs http.FileSystem, name string, redirect bo } // else, set the last modified as "serveContent" does. - context.SetLastModified(ctx, d.ModTime()) + ctx.SetLastModified(d.ModTime()) // write the file to the response writer. contents, err := ioutil.ReadAll(f) diff --git a/iris.go b/iris.go index 40431e3c..35316c6f 100644 --- a/iris.go +++ b/iris.go @@ -360,11 +360,32 @@ var ( // // A shortcut for the `handlerconv#FromStd`. FromStd = handlerconv.FromStd - // Cache is a middleware providing cache functionalities + // Cache is a middleware providing server-side cache functionalities // to the next handlers, can be used as: `app.Get("/", iris.Cache, aboutHandler)`. + // It should be used after Static methods. + // See `context#Cache304` for an alternative, faster way. // // Examples can be found at: https://github.com/kataras/iris/tree/master/_examples/#caching Cache = cache.Handler + // Cache304 sends a `StatusNotModified` (304) whenever + // the "If-Modified-Since" request header (time) is before the + // time.Now() + expiresEvery (always compared to their UTC values). + // Use this, which is a shortcut of the, `context#Cache304` instead of the "github.com/kataras/iris/cache" or iris.Cache + // for better performance. + // Clients that are compatible with the http RCF (all browsers are and tools like postman) + // will handle the caching. + // The only disadvantage of using that instead of server-side caching + // is that this method will send a 304 status code instead of 200, + // So, if you use it side by side with other micro services + // you have to check for that status code as well for a valid response. + // + // Developers are free to extend this method's behavior + // by watching system directories changes manually and use of the `ctx.WriteWithExpiration` + // with a "modtime" based on the file modified date, + // simillary to the `StaticWeb`(StaticWeb sends an OK(200) and browser disk caching instead of 304). + // + // A shortcut of the `context#Cache304`. + Cache304 = context.Cache304 ) // SPA accepts an "assetHandler" which can be the result of an From 687477f21009234200124d5f673056300f0622cc Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Sun, 28 Jan 2018 15:43:25 +0200 Subject: [PATCH 11/17] fix comment on csrf example Former-commit-id: c5c001cbc5381fb169429f132ea18c4fc7c76a42 --- _examples/experimental-handlers/csrf/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_examples/experimental-handlers/csrf/main.go b/_examples/experimental-handlers/csrf/main.go index c9e6a7d5..e8a8bf00 100644 --- a/_examples/experimental-handlers/csrf/main.go +++ b/_examples/experimental-handlers/csrf/main.go @@ -39,7 +39,7 @@ func main() { } func getSignupForm(ctx iris.Context) { - // views/signup.html just needs a {{ .csrfField }} template tag for + // views/user/signup.html just needs a {{ .csrfField }} template tag for // csrf.TemplateField to inject the CSRF token into. Easy! ctx.ViewData(csrf.TemplateTag, csrf.TemplateField(ctx)) ctx.View("user/signup.html") From e38ea65dc70c974546de5209d69c5b33b32ea8b0 Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Tue, 30 Jan 2018 21:16:54 +0200 Subject: [PATCH 12/17] fix example comment on routing/dynamic-path/main.go#L101 Former-commit-id: affb9b646e83ab9087d4c84aac8c12e11468257b --- _examples/routing/dynamic-path/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_examples/routing/dynamic-path/main.go b/_examples/routing/dynamic-path/main.go index b7875b9a..b7e78c40 100644 --- a/_examples/routing/dynamic-path/main.go +++ b/_examples/routing/dynamic-path/main.go @@ -98,7 +98,7 @@ func main() { // // {param:string equal(iris)} , "iris" will be the argument here: // app.Macros().String.RegisterFunc("equal", func(argument string) func(paramValue string) bool { - // return func(paramValue string){ return argument == paramValue } + // return func(paramValue string) bool { return argument == paramValue } // }) // you can use the "string" type which is valid for a single path parameter that can be anything. From 019911237c3aa45bd9559b998059d811787016b0 Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Wed, 31 Jan 2018 02:35:22 +0200 Subject: [PATCH 13/17] add context#StatusCodeNotSuccessful for customize even the most customized clients that are not compatible with the standards and fix the `SPA` if static file serve handlers are passed as its `AssetHandler` as reported at the chat.iris-go.com Former-commit-id: ccd0815a09b9305bfbeaad7b46559dd86f34f20b --- .../single-page-application/basic/main.go | 2 +- _examples/routing/README.md | 3 +- .../bootstrap/bootstrap/bootstrapper.go | 3 +- configuration.go | 7 +- context/context.go | 17 +- context/response_recorder.go | 2 +- context/transaction.go | 2 +- core/router/api_builder.go | 69 ++------ core/router/fs.go | 2 +- core/router/router_spa_wrapper.go | 91 ---------- core/router/spa.go | 167 ++++++++++++++++++ core/router/status.go | 12 +- 12 files changed, 213 insertions(+), 164 deletions(-) delete mode 100644 core/router/router_spa_wrapper.go create mode 100644 core/router/spa.go diff --git a/_examples/file-server/single-page-application/basic/main.go b/_examples/file-server/single-page-application/basic/main.go index 6ed45228..649f7066 100644 --- a/_examples/file-server/single-page-application/basic/main.go +++ b/_examples/file-server/single-page-application/basic/main.go @@ -21,7 +21,7 @@ func newApp() *iris.Application { }) // or just serve index.html as it is: - // app.Get("/", func(ctx iris.Context) { + // app.Get("/{f:path}", func(ctx iris.Context) { // ctx.ServeFile("index.html", false) // }) diff --git a/_examples/routing/README.md b/_examples/routing/README.md index f57c4849..5b85c202 100644 --- a/_examples/routing/README.md +++ b/_examples/routing/README.md @@ -611,7 +611,8 @@ func main(){ app := iris.New() app.OnErrorCode(iris.StatusNotFound, notFound) app.OnErrorCode(iris.StatusInternalServerError, internalServerError) - // to register a handler for all status codes >=400: + // to register a handler for all "error" status codes(context.StatusCodeNotSuccessful) + // defaults to < 200 || >= 400: // app.OnAnyErrorCode(handler) app.Get("/", index) app.Run(iris.Addr(":8080")) diff --git a/_examples/structuring/bootstrap/bootstrap/bootstrapper.go b/_examples/structuring/bootstrap/bootstrap/bootstrapper.go index 46f26fbf..9efa7df6 100644 --- a/_examples/structuring/bootstrap/bootstrap/bootstrapper.go +++ b/_examples/structuring/bootstrap/bootstrap/bootstrapper.go @@ -64,7 +64,8 @@ func (b *Bootstrapper) SetupWebsockets(endpoint string, onConnection websocket.C }) } -// SetupErrorHandlers prepares the http error handlers (>=400). +// SetupErrorHandlers prepares the http error handlers +// `(context.StatusCodeNotSuccessful`, which defaults to < 200 || >= 400 but you can change it). func (b *Bootstrapper) SetupErrorHandlers() { b.OnAnyErrorCode(func(ctx iris.Context) { err := iris.Map{ diff --git a/configuration.go b/configuration.go index 2d8e8cde..2ab09e3b 100644 --- a/configuration.go +++ b/configuration.go @@ -460,13 +460,14 @@ type Configuration struct { DisableBodyConsumptionOnUnmarshal bool `json:"disableBodyConsumptionOnUnmarshal,omitempty" yaml:"DisableBodyConsumptionOnUnmarshal" toml:"DisableBodyConsumptionOnUnmarshal"` // DisableAutoFireStatusCode if true then it turns off the http error status code handler automatic execution - // from "context.StatusCode(>=400)" and instead app should manually call the "context.FireStatusCode(>=400)". + // from (`context.StatusCodeNotSuccessful`, defaults to < 200 || >= 400). + // If that is false then for a direct error firing, then call the "context#FireStatusCode(statusCode)" manually. // // By-default a custom http error handler will be fired when "context.StatusCode(code)" called, - // code should be >=400 in order to be received as an "http error handler". + // code should be equal with the result of the the `context.StatusCodeNotSuccessful` in order to be received as an "http error handler". // // Developer may want this option to setted as true in order to manually call the - // error handlers when needed via "context.FireStatusCode(>=400)". + // error handlers when needed via "context#FireStatusCode(< 200 || >= 400)". // HTTP Custom error handlers are being registered via app.OnErrorCode(code, handler)". // // Defaults to false. diff --git a/context/context.go b/context/context.go index 35a91896..05ea0174 100644 --- a/context/context.go +++ b/context/context.go @@ -1042,6 +1042,21 @@ func (ctx *context) BeginRequest(w http.ResponseWriter, r *http.Request) { ctx.writer.BeginResponse(w) } +// StatusCodeNotSuccessful defines if a specific "statusCode" is not +// a valid status code for a successful response. +// It defaults to < 200 || >= 400 +// +// Read more at `iris#DisableAutoFireStatusCode`, `iris/core/router#ErrorCodeHandler` +// and `iris/core/router#OnAnyErrorCode` for relative information. +// +// Do NOT change it. +// +// It's exported for extreme situations--special needs only, when the Iris server and the client +// is not following the RFC: https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html +var StatusCodeNotSuccessful = func(statusCode int) bool { + return statusCode < 200 || statusCode >= 400 +} + // EndRequest is executing once after a response to the request was sent and this context is useless or released. // // To follow the iris' flow, developer should: @@ -1049,7 +1064,7 @@ func (ctx *context) BeginRequest(w http.ResponseWriter, r *http.Request) { // 2. release the response writer // and any other optional steps, depends on dev's application type. func (ctx *context) EndRequest() { - if ctx.GetStatusCode() >= 400 && + if StatusCodeNotSuccessful(ctx.GetStatusCode()) && !ctx.Application().ConfigurationReadOnly().GetDisableAutoFireStatusCode() { // author's note: // if recording, the error handler can handle diff --git a/context/response_recorder.go b/context/response_recorder.go index c469dc25..0157a94f 100644 --- a/context/response_recorder.go +++ b/context/response_recorder.go @@ -189,7 +189,7 @@ func (w *ResponseRecorder) WriteTo(res ResponseWriter) { if to, ok := res.(*ResponseRecorder); ok { - // set the status code, to is first ( probably an error >=400) + // set the status code, to is first ( probably an error? (context.StatusCodeNotSuccessful, defaults to < 200 || >= 400). if statusCode := w.ResponseWriter.StatusCode(); statusCode == defaultStatusCode { to.WriteHeader(statusCode) } diff --git a/context/transaction.go b/context/transaction.go index bd0442b1..e0f580ac 100644 --- a/context/transaction.go +++ b/context/transaction.go @@ -20,7 +20,7 @@ func (err TransactionErrResult) Error() string { // IsFailure returns true if this is an actual error func (err TransactionErrResult) IsFailure() bool { - return err.StatusCode >= 400 + return StatusCodeNotSuccessful(err.StatusCode) } // NewTransactionErrResult returns a new transaction result with the given error message, diff --git a/core/router/api_builder.go b/core/router/api_builder.go index 5864aa1e..eed18a56 100644 --- a/core/router/api_builder.go +++ b/core/router/api_builder.go @@ -686,16 +686,6 @@ func (api *APIBuilder) StaticWeb(requestPath string, systemPath string) *Route { handler := func(ctx context.Context) { h(ctx) - // if ctx.GetStatusCode() >= 200 && ctx.GetStatusCode() < 400 { - // // re-check the content type here for any case, - // // although the new code does it automatically but it's good to have it here. - // if _, exists := ctx.ResponseWriter().Header()["Content-Type"]; !exists { - // if fname := ctx.Params().Get(paramName); fname != "" { - // cType := TypeByFilename(fname) - // ctx.ContentType(cType) - // } - // } - // } } requestPath = joinPath(requestPath, WildcardParam(paramName)) @@ -703,7 +693,7 @@ func (api *APIBuilder) StaticWeb(requestPath string, systemPath string) *Route { } // OnErrorCode registers an error http status code -// based on the "statusCode" >= 400. +// based on the "statusCode" < 200 || >= 400 (came from `context.StatusCodeNotSuccessful`). // The handler is being wrapepd by a generic // handler which will try to reset // the body if recorder was enabled @@ -718,55 +708,16 @@ func (api *APIBuilder) OnErrorCode(statusCode int, handlers ...context.Handler) } // OnAnyErrorCode registers a handler which called when error status code written. -// Same as `OnErrorCode` but registers all http error codes. -// See: http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml +// Same as `OnErrorCode` but registers all http error codes based on the `context.StatusCodeNotSuccessful` +// which defaults to < 200 || >= 400 for an error code, any previos error code will be overriden, +// so call it first if you want to use any custom handler for a specific error status code. +// +// Read more at: http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml func (api *APIBuilder) OnAnyErrorCode(handlers ...context.Handler) { - // we could register all >=400 and <=511 but this way - // could override custom status codes that iris developers can register for their - // web apps whenever needed. - // There fore these are the hard coded http error statuses: - var errStatusCodes = []int{ - http.StatusBadRequest, - http.StatusUnauthorized, - http.StatusPaymentRequired, - http.StatusForbidden, - http.StatusNotFound, - http.StatusMethodNotAllowed, - http.StatusNotAcceptable, - http.StatusProxyAuthRequired, - http.StatusRequestTimeout, - http.StatusConflict, - http.StatusGone, - http.StatusLengthRequired, - http.StatusPreconditionFailed, - http.StatusRequestEntityTooLarge, - http.StatusRequestURITooLong, - http.StatusUnsupportedMediaType, - http.StatusRequestedRangeNotSatisfiable, - http.StatusExpectationFailed, - http.StatusTeapot, - http.StatusUnprocessableEntity, - http.StatusLocked, - http.StatusFailedDependency, - http.StatusUpgradeRequired, - http.StatusPreconditionRequired, - http.StatusTooManyRequests, - http.StatusRequestHeaderFieldsTooLarge, - http.StatusUnavailableForLegalReasons, - http.StatusInternalServerError, - http.StatusNotImplemented, - http.StatusBadGateway, - http.StatusServiceUnavailable, - http.StatusGatewayTimeout, - http.StatusHTTPVersionNotSupported, - http.StatusVariantAlsoNegotiates, - http.StatusInsufficientStorage, - http.StatusLoopDetected, - http.StatusNotExtended, - http.StatusNetworkAuthenticationRequired} - - for _, statusCode := range errStatusCodes { - api.OnErrorCode(statusCode, handlers...) + for code := 100; code <= 511; code++ { + if context.StatusCodeNotSuccessful(code) { + api.OnErrorCode(code, handlers...) + } } } diff --git a/core/router/fs.go b/core/router/fs.go index ace1a494..d4745de4 100644 --- a/core/router/fs.go +++ b/core/router/fs.go @@ -254,7 +254,7 @@ func (w *fsHandler) Build() context.Handler { gzipEnabled) // check for any http errors after the file handler executed - if prevStatusCode >= 400 { // error found (404 or 400 or 500 usually) + if context.StatusCodeNotSuccessful(prevStatusCode) { // error found (404 or 400 or 500 usually) if writer, ok := ctx.ResponseWriter().(*context.GzipResponseWriter); ok && writer != nil { writer.ResetBody() writer.Disable() diff --git a/core/router/router_spa_wrapper.go b/core/router/router_spa_wrapper.go deleted file mode 100644 index 16b43e89..00000000 --- a/core/router/router_spa_wrapper.go +++ /dev/null @@ -1,91 +0,0 @@ -package router - -import ( - "strings" - - "github.com/kataras/iris/context" -) - -// AssetValidator returns true if "filename" -// is asset, i.e: strings.Contains(filename, "."). -type AssetValidator func(filename string) bool - -// SPABuilder helps building a single page application server -// which serves both routes and files from the root path. -type SPABuilder struct { - IndexNames []string - AssetHandler context.Handler - AssetValidators []AssetValidator -} - -// AddIndexName will add an index name. -// If path == $filename then it redirects to "/". -// -// It can be called after the `BuildWrapper ` as well but BEFORE the server start. -func (s *SPABuilder) AddIndexName(filename string) *SPABuilder { - s.IndexNames = append(s.IndexNames, filename) - return s -} - -// NewSPABuilder returns a new Single Page Application builder -// It does what StaticWeb or StaticEmbedded expected to do when serving files and routes at the same time -// from the root "/" path. -// -// Accepts a static asset handler, which can be an app.StaticHandler, app.StaticEmbeddedHandler... -func NewSPABuilder(assetHandler context.Handler) *SPABuilder { - if assetHandler == nil { - assetHandler = func(ctx context.Context) { - ctx.Writef("empty asset handler") - } - } - - return &SPABuilder{ - IndexNames: nil, - // IndexNames is empty by-default, - // if the user wants to redirect to "/" from "/index.html" she/he can chage that to []string{"index.html"} manually. - AssetHandler: assetHandler, - AssetValidators: []AssetValidator{ - func(path string) bool { - return true // returns true by-default, if false then it fires 404. - }, - }, - } -} - -func (s *SPABuilder) isAsset(reqPath string) bool { - for _, v := range s.AssetValidators { - if !v(reqPath) { - return false - } - } - return true -} - -// Handler serves the asset handler but in addition, it makes some checks before that, -// based on the `AssetValidators` and `IndexNames`. -func (s *SPABuilder) Handler(ctx context.Context) { - path := ctx.Path() - - // make a validator call, by-default all paths are valid and this codeblock doesn't mean anything - // but for cases that users wants to bypass an asset she/he can do that by modifiying the `APIBuilder#AssetValidators` field. - // - // It's here for backwards compatibility as well, see #803. - if !s.isAsset(path) { - // it's not asset, execute the registered route's handlers - ctx.NotFound() - return - } - - for _, index := range s.IndexNames { - if strings.HasSuffix(path, index) { - localRedirect(ctx, "./") - // "/" should be manually registered. - // We don't setup an index handler here, - // let full control to the user - // (use middleware, ctx.ServeFile or ctx.View and so on...) - return - } - } - - s.AssetHandler(ctx) -} diff --git a/core/router/spa.go b/core/router/spa.go new file mode 100644 index 00000000..0ff8ad5c --- /dev/null +++ b/core/router/spa.go @@ -0,0 +1,167 @@ +package router + +import ( + "strings" + + "github.com/kataras/iris/context" +) + +// AssetValidator returns true if "filename" +// is asset, i.e: strings.Contains(filename, "."). +type AssetValidator func(filename string) bool + +// SPABuilder helps building a single page application server +// which serves both routes and files from the root path. +type SPABuilder struct { + // Root defaults to "/", it's the root path that explicitly set-ed, + // this can be changed if more than SPAs are used on the same + // iris router instance. + Root string + // emptyRoot can be changed with `ChangeRoot` only, + // is, statically, true if root is empty + // and if root is empty then let 404 fire from server-side anyways if + // the passed `AssetHandler` returns 404 for a specific request path. + // Defaults to false. + emptyRoot bool + + IndexNames []string + AssetHandler context.Handler + AssetValidators []AssetValidator +} + +// AddIndexName will add an index name. +// If path == $filename then it redirects to Root, which defaults to "/". +// +// It can be called BEFORE the server start. +func (s *SPABuilder) AddIndexName(filename string) *SPABuilder { + s.IndexNames = append(s.IndexNames, filename) + return s +} + +// ChangeRoot modifies the `Root` request path that is +// explicitly set-ed if the `AssetHandler` gave a Not Found (404) +// previously, if request's path is the passed "path" +// then it explicitly sets that and it retries executing the `AssetHandler`. +// +// Empty Root means that let 404 fire from server-side anyways. +// +// Change it ONLY if you use more than one typical SPAs on the same Iris Application instance. +func (s *SPABuilder) ChangeRoot(path string) *SPABuilder { + s.Root = path + s.emptyRoot = path == "" + return s +} + +// NewSPABuilder returns a new Single Page Application builder +// It does what StaticWeb or StaticEmbedded expected to do when serving files and routes at the same time +// from the root "/" path. +// +// Accepts a static asset handler, which can be an app.StaticHandler, app.StaticEmbeddedHandler... +func NewSPABuilder(assetHandler context.Handler) *SPABuilder { + if assetHandler == nil { + assetHandler = func(ctx context.Context) { + ctx.Writef("empty asset handler") + } + } + + return &SPABuilder{ + Root: "/", + IndexNames: nil, + // "IndexNames" are empty by-default, + // if the user wants to redirect to "/" from "/index.html" she/he can chage that to []string{"index.html"} manually + // or use the `StaticHandler` as "AssetHandler" which does that already. + AssetHandler: assetHandler, + AssetValidators: []AssetValidator{ + func(path string) bool { + return true // returns true by-default, if false then it fires 404. + }, + }, + } +} + +func (s *SPABuilder) isAsset(reqPath string) bool { + for _, v := range s.AssetValidators { + if !v(reqPath) { + return false + } + } + return true +} + +// Handler serves the asset handler but in addition, it makes some checks before that, +// based on the `AssetValidators` and `IndexNames`. +func (s *SPABuilder) Handler(ctx context.Context) { + path := ctx.Path() + + // make a validator call, by-default all paths are valid and this codeblock doesn't mean anything + // but for cases that users wants to bypass an asset she/he can do that by modifiying the `APIBuilder#AssetValidators` field. + // + // It's here for backwards compatibility as well, see #803. + if !s.isAsset(path) { + // it's not asset, execute the registered route's handlers + ctx.NotFound() + return + } + + for _, index := range s.IndexNames { + if strings.HasSuffix(path, index) { + if s.emptyRoot { + ctx.NotFound() + return + } + localRedirect(ctx, "."+s.Root) + // s.Root should be manually registered to a route + // (not always, only if custom handler used). + // We don't setup an index handler here, + // let full control to the developer via "AssetHandler" + // (use of middleware, manually call of the ctx.ServeFile or ctx.View etc.) + return + } + } + + s.AssetHandler(ctx) + + if context.StatusCodeNotSuccessful(ctx.GetStatusCode()) && !s.emptyRoot && path != s.Root { + // If file was not something like a javascript file, or a css or anything that + // the passed `AssetHandler` scan-ed then re-execute the `AssetHandler` + // using the `Root` as the request path (virtually). + // + // If emptyRoot is true then + // fire the response as it's, "AssetHandler" is fully responsible for it, + // client-side's router for invalid paths will not work here else read below. + // + // Author's notes: + // the server doesn't need to know all client routes, + // client-side router is responsible for any kind of invalid paths, + // so explicit set to root path. + // + // The most simple solution was to use a + // func(ctx iris.Context) { ctx.ServeFile("$PATH/index.html") } as the "AssetHandler" + // but many developers use the `StaticHandler` (as shown in the examples) + // but it was not working as expected because it (correctly) fires + // a 404 not found if a file based on the request path didn't found. + // + // We can't just do it before the "AssetHandler"'s execution + // for two main reasons: + // 1. if it's a file serve handler, like `StaticHandler` then it will never serve + // the corresponding files! + // 2. it may manually handle those things, + // don't forget that "AssetHandler" can be + // ANY iris handler, so we can't be sure what the developer may want to do there. + // + // "AssetHandler" as the "StaticHandler" a retry doesn't hurt, + // it will give us a 404 if the file didn't found very fast WITHOUT moving to the + // rest of its validation and serving implementation. + // + // Another idea would be to modify the "AssetHandler" on every `ChangeRoot` + // call, which may give us some performance (ns) benefits + // but this could be bad if root is set-ed before the "AssetHandler", + // so keep it as it's. + rootURL, err := ctx.Request().URL.Parse(s.Root) + if err == nil { + ctx.Request().URL = rootURL + s.AssetHandler(ctx) + } + + } +} diff --git a/core/router/status.go b/core/router/status.go index 2099a78d..fc41fe81 100644 --- a/core/router/status.go +++ b/core/router/status.go @@ -7,6 +7,10 @@ import ( "github.com/kataras/iris/context" ) +func statusCodeSuccessful(statusCode int) bool { + return !context.StatusCodeNotSuccessful(statusCode) +} + // ErrorCodeHandler is the entry // of the list of all http error code handlers. type ErrorCodeHandler struct { @@ -21,7 +25,7 @@ type ErrorCodeHandler struct { func (ch *ErrorCodeHandler) Fire(ctx context.Context) { // if we can reset the body if w, ok := ctx.IsRecording(); ok { - if w.StatusCode() < 400 { // if not an error status code + if statusCodeSuccessful(w.StatusCode()) { // if not an error status code w.WriteHeader(ch.StatusCode) // then set it manually here, otherwise it should be setted via ctx.StatusCode(...) } // reset if previous content and it's recorder, keep the status code. @@ -109,14 +113,14 @@ func (s *ErrorCodeHandlers) Get(statusCode int) *ErrorCodeHandler { } // Register registers an error http status code -// based on the "statusCode" >= 400. +// based on the "statusCode" < 200 || >= 400 (`context.StatusCodeNotSuccessful`). // The handler is being wrapepd by a generic // handler which will try to reset // the body if recorder was enabled // and/or disable the gzip if gzip response recorder // was active. func (s *ErrorCodeHandlers) Register(statusCode int, handlers ...context.Handler) *ErrorCodeHandler { - if statusCode < 400 { + if statusCodeSuccessful(statusCode) { return nil } @@ -145,7 +149,7 @@ func (s *ErrorCodeHandlers) Register(statusCode int, handlers ...context.Handler // then it creates & registers a new trivial handler on the-fly. func (s *ErrorCodeHandlers) Fire(ctx context.Context) { statusCode := ctx.GetStatusCode() - if statusCode < 400 { + if statusCodeSuccessful(statusCode) { return } ch := s.Get(statusCode) From 711e48d9ab106e39eeea4dc06383aaa8a4f8fa9c Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Sat, 3 Feb 2018 18:31:01 +0200 Subject: [PATCH 14/17] fix https://github.com/kataras/iris/issues/897 Former-commit-id: 6f94a79d9ce3ac9b2b05d1e2637059996a02df1d --- _examples/experimental-handlers/csrf/main.go | 2 +- context/context.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/_examples/experimental-handlers/csrf/main.go b/_examples/experimental-handlers/csrf/main.go index e8a8bf00..20964ac0 100644 --- a/_examples/experimental-handlers/csrf/main.go +++ b/_examples/experimental-handlers/csrf/main.go @@ -44,7 +44,7 @@ func getSignupForm(ctx iris.Context) { ctx.ViewData(csrf.TemplateTag, csrf.TemplateField(ctx)) ctx.View("user/signup.html") - // We could also retrieve the token directly from csrf.Token(r) and + // We could also retrieve the token directly from csrf.Token(ctx) and // set it in the request header - ctx.GetHeader("X-CSRF-Token", token) // This is useful if you're sending JSON to clients or a front-end JavaScript // framework. diff --git a/context/context.go b/context/context.go index 05ea0174..541b1f42 100644 --- a/context/context.go +++ b/context/context.go @@ -1553,8 +1553,7 @@ func (ctx *context) ContentType(cType string) { // if doesn't contain a charset already then append it if !strings.Contains(cType, "charset") { if cType != ContentBinaryHeaderValue { - charset := ctx.Application().ConfigurationReadOnly().GetCharset() - cType += "; charset=" + charset + cType += "; charset=" + ctx.Application().ConfigurationReadOnly().GetCharset() } } @@ -1940,6 +1939,7 @@ func (ctx *context) UploadFormFiles(destDirectory string, before ...func(Context n += n0 } } + return n, nil } } From e523d08cb1fb7ea6bd8ee154667836f575e32c18 Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Sat, 3 Feb 2018 18:53:39 +0200 Subject: [PATCH 15/17] add websocket/Connection#IsJoined as requested at https://github.com/kataras/iris/issues/895 Former-commit-id: 560fc8b911a9c1352be577d2f7bebd1fac7b5d4a --- websocket/connection.go | 7 +++++++ websocket/server.go | 22 ++++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/websocket/connection.go b/websocket/connection.go index ce887e95..a38decd7 100644 --- a/websocket/connection.go +++ b/websocket/connection.go @@ -174,6 +174,9 @@ type ( On(string, MessageFunc) // Join registers this connection to a room, if it doesn't exist then it creates a new. One room can have one or more connections. One connection can be joined to many rooms. All connections are joined to a room specified by their `ID` automatically. Join(string) + // IsJoined returns true when this connection is joined to the room, otherwise false. + // It Takes the room name as its input parameter. + IsJoined(roomName string) bool // Leave removes this connection entry from a room // Returns true if the connection has actually left from the particular room. Leave(string) bool @@ -506,6 +509,10 @@ func (c *connection) Join(roomName string) { c.server.Join(roomName, c.id) } +func (c *connection) IsJoined(roomName string) bool { + return c.server.IsJoined(roomName, c.id) +} + func (c *connection) Leave(roomName string) bool { return c.server.Leave(roomName, c.id) } diff --git a/websocket/server.go b/websocket/server.go index aec5de33..a08b3e1a 100644 --- a/websocket/server.go +++ b/websocket/server.go @@ -256,6 +256,28 @@ func (s *Server) join(roomName string, connID string) { s.rooms[roomName] = append(s.rooms[roomName], connID) } +// IsJoined reports if a specific room has a specific connection into its values. +// First parameter is the room name, second is the connection's id. +// +// It returns true when the "connID" is joined to the "roomName". +func (s *Server) IsJoined(roomName string, connID string) bool { + s.mu.RLock() + room := s.rooms[roomName] + s.mu.RUnlock() + + if room == nil { + return false + } + + for _, connid := range room { + if connID == connid { + return true + } + } + + return false +} + // LeaveAll kicks out a connection from ALL of its joined rooms func (s *Server) LeaveAll(connID string) { s.mu.Lock() From 431e339ccc2acace4069c0ce3f14477782848f9e Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Tue, 6 Feb 2018 11:55:56 +0200 Subject: [PATCH 16/17] don't force-set content type on gzip response writer's WriteString and Writef if already there Former-commit-id: c882a6ef14e89dd0da7a3a2afc85100ca07dc869 --- Gopkg.lock | 2 +- context/gzip_response_writer.go | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/Gopkg.lock b/Gopkg.lock index c77e0692..6bf5e157 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -170,7 +170,7 @@ revision = "abc90934186a77966e2beeac62ed966aac0561d5" [[projects]] - branch = "master" + branch = "v1.2.0" name = "github.com/satori/go.uuid" packages = ["."] revision = "f58768cc1a7a7e77a3bd49e98cdd21419399b6a3" diff --git a/context/gzip_response_writer.go b/context/gzip_response_writer.go index 05ee0c65..c9582f99 100644 --- a/context/gzip_response_writer.go +++ b/context/gzip_response_writer.go @@ -117,7 +117,9 @@ func (w *GzipResponseWriter) Write(contents []byte) (int, error) { func (w *GzipResponseWriter) Writef(format string, a ...interface{}) (n int, err error) { n, err = fmt.Fprintf(w, format, a...) if err == nil { - w.ResponseWriter.Header().Set(contentTypeHeaderKey, ContentTextHeaderValue) + if w.ResponseWriter.Header()[contentTypeHeaderKey] == nil { + w.ResponseWriter.Header().Set(contentTypeHeaderKey, ContentTextHeaderValue) + } } return @@ -128,7 +130,10 @@ func (w *GzipResponseWriter) Writef(format string, a ...interface{}) (n int, err func (w *GzipResponseWriter) WriteString(s string) (n int, err error) { n, err = w.Write([]byte(s)) if err == nil { - w.ResponseWriter.Header().Set(contentTypeHeaderKey, ContentTextHeaderValue) + if w.ResponseWriter.Header()[contentTypeHeaderKey] == nil { + w.ResponseWriter.Header().Set(contentTypeHeaderKey, ContentTextHeaderValue) + } + } return } From 5fa306d69d5094f9075f2259f4320ee2da887b90 Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Tue, 6 Feb 2018 12:38:02 +0200 Subject: [PATCH 17/17] Update to version 10.1.0. Happy February to Everyone!! Former-commit-id: 9c2a95f10c7299d21085d9d5cb4f1327f917123d --- HISTORY.md | 23 +++++++++++++++++++++++ HISTORY_GR.md | 4 ++++ HISTORY_ZH.md | 4 ++++ README.md | 4 ++-- VERSION | 2 +- core/maintenance/version.go | 2 +- core/router/api_builder.go | 2 +- 7 files changed, 36 insertions(+), 5 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index 1ae89303..18dc21fa 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -17,6 +17,29 @@ Developers are not forced to upgrade if they don't really need it. Upgrade whene **How to upgrade**: Open your command-line and execute this command: `go get -u github.com/kataras/iris` or let the automatic updater do that for you. +# Tu, 06 February 2018 | v10.1.0 + +New Features: + +- Multi-Level subdomain redirect helper, you can find an example [here](https://github.com/kataras/iris/blob/master/_examples/subdomains/redirect/main.go) +- Cache middleware which makes use of the `304` status code, request fires from client to server but server respond with a status code, client is responsible to render the cached, you can find an example [here](https://github.com/kataras/iris/blob/master/_examples/cache/client-side/main.go) +- `websocket/Connection#IsJoined(roomName string)` new method to check if a user is joined to a room. An un-joined connections cannot send messages, this check is optionally. + +More: + +- update vendor/golang/crypto package to its latest version again, they have a lot of fixes there, as you know we're always following the dependencies for any fixes and meanful updates. +- [don't force-set content type on gzip response writer's WriteString and Writef if already there](https://github.com/kataras/iris/commit/af79aad11932f1a4fcbf7ebe28274b96675d0000) +- [new: add websocket/Connection#IsJoined](https://github.com/kataras/iris/commit/cb9e30948c8f1dd099f5168218d110765989992e) +- [fix #897](https://github.com/kataras/iris/commit/21cb572b638e82711910745cfae3c52d836f01f9) +- [add context#StatusCodeNotSuccessful variable for customize even the rfc2616-sec10](https://github.com/kataras/iris/commit/c56b7a3f04d953a264dfff15dadd2b4407d62a6f) +- [fix example comment on routing/dynamic-path/main.go#L101](https://github.com/kataras/iris/commit/0fbf1d45f7893cb1393759b7362444f3d381d182) +- [new: Cache Middleware `iris.Cache304`](https://github.com/kataras/iris/commit/1722355870174cecbc12f7beff8514b058b3b912) +- [fix comment on csrf example](https://github.com/kataras/iris/commit/a39e3d7d6cf528e51e6c7e32a884a8d9f2fadc0b) +- [un-default the Configuration.RemoteAddrHeaders](https://github.com/kataras/iris/commit/47108dc5a147a8b23de61bef86fe9327f0781396) +- [add vscode extension link and badge](https://github.com/kataras/iris/commit/6f594c0a7c641cc98bd683163fffbf5fa5fc8de6) +- [add an `app.View` example for parsing and writing templates outside of the HTTP (similar to context#View)](_examples/view/write-to) +- [new: Support multi-level subdomains redirect](https://github.com/kataras/iris/commit/12d7df113e611a75088c2a72774dab749d2c7685). + # Tu, 16 January 2018 | v10.0.2 ## Security | `iris.AutoTLS` diff --git a/HISTORY_GR.md b/HISTORY_GR.md index e3fd5492..85cf7565 100644 --- a/HISTORY_GR.md +++ b/HISTORY_GR.md @@ -17,6 +17,10 @@ **Πώς να αναβαθμίσετε**: Ανοίξτε την γραμμή εντολών σας και εκτελέστε αυτήν την εντολή: `go get -u github.com/kataras/iris` ή αφήστε το αυτόματο updater να το κάνει αυτό για σας. +# Tu, 06 February 2018 | v10.1.0 + +This history entry is not yet translated to Chinese. Please read [the english version instead](https://github.com/kataras/iris/blob/master/HISTORY.md#tu-06-february-2018--v1010). + # Tu, 16 January 2018 | v10.0.2 ## Ασφάλεια | `iris.AutoTLS` diff --git a/HISTORY_ZH.md b/HISTORY_ZH.md index f65c4ecc..4dfaa983 100644 --- a/HISTORY_ZH.md +++ b/HISTORY_ZH.md @@ -17,6 +17,10 @@ **如何升级**: 打开命令行执行以下命令: `go get -u github.com/kataras/iris` 或者等待自动更新。 +# Tu, 06 February 2018 | v10.1.0 + +This history entry is not yet translated to Chinese. Please read [the english version instead](https://github.com/kataras/iris/blob/master/HISTORY.md#tu-06-february-2018--v1010). + # 2018 1月16号 | v10.0.2 版本更新 ## 安全更新 | `iris.AutoTLS` diff --git a/README.md b/README.md index 52bdccea..98c7f37a 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ -[![build status](https://img.shields.io/travis/kataras/iris/master.svg?style=flat-square)](https://travis-ci.org/kataras/iris) [![report card](https://img.shields.io/badge/report%20card-a%2B-ff3333.svg?style=flat-square)](http://goreportcard.com/report/kataras/iris) [![vscode-iris](https://img.shields.io/badge/ext%20-vscode-0c77e3.svg?style=flat-square)](https://marketplace.visualstudio.com/items?itemName=kataras2006.iris) [![chat](https://img.shields.io/badge/community-%20chat-00BCD4.svg?style=flat-square)](https://kataras.rocket.chat/channel/iris) [![view examples](https://img.shields.io/badge/learn%20by-examples-0077b3.svg?style=flat-square)](_examples/) [![release](https://img.shields.io/badge/release%20-v10.0-0077b3.svg?style=flat-square)](https://github.com/kataras/iris/releases) +[![build status](https://img.shields.io/travis/kataras/iris/master.svg?style=flat-square)](https://travis-ci.org/kataras/iris) [![report card](https://img.shields.io/badge/report%20card-a%2B-ff3333.svg?style=flat-square)](http://goreportcard.com/report/kataras/iris) [![vscode-iris](https://img.shields.io/badge/ext%20-vscode-0c77e3.svg?style=flat-square)](https://marketplace.visualstudio.com/items?itemName=kataras2006.iris) [![chat](https://img.shields.io/badge/community-%20chat-00BCD4.svg?style=flat-square)](https://kataras.rocket.chat/channel/iris) [![view examples](https://img.shields.io/badge/learn%20by-examples-0077b3.svg?style=flat-square)](_examples/) [![release](https://img.shields.io/badge/release%20-v10.1-0077b3.svg?style=flat-square)](https://github.com/kataras/iris/releases) Iris is a fast, simple yet fully featured and very efficient web framework for Go. @@ -106,7 +106,7 @@ _Updated at: [Tuesday, 21 November 2017](_benchmarks/README_UNIX.md)_ ## Support -- [HISTORY](HISTORY.md#tu-16-jenuary-2018--v1002) file is your best friend, it contains information about the latest features and changes +- [HISTORY](HISTORY.md#tu-06-february-2018--v1010) file is your best friend, it contains information about the latest features and changes - Did you happen to find a bug? Post it at [github issues](https://github.com/kataras/iris/issues) - Do you have any questions or need to speak with someone experienced to solve a problem at real-time? Join us to the [community chat](https://chat.iris-go.com) - Complete our form-based user experience report by clicking [here](https://docs.google.com/forms/d/e/1FAIpQLSdCxZXPANg_xHWil4kVAdhmh7EBBHQZ_4_xSZVDL-oCC_z5pA/viewform?usp=sf_link) diff --git a/VERSION b/VERSION index b9a02d3a..943c0990 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -10.0.2:https://github.com/kataras/iris/blob/master/HISTORY.md#tu-16-jenuary-2018--v1002 \ No newline at end of file +10.1.0:https://github.com/kataras/iris/blob/master/HISTORY.md#tu-06-february-2018--v1010 \ No newline at end of file diff --git a/core/maintenance/version.go b/core/maintenance/version.go index a12c7ad1..f98c41ab 100644 --- a/core/maintenance/version.go +++ b/core/maintenance/version.go @@ -13,7 +13,7 @@ import ( const ( // Version is the string representation of the current local Iris Web Framework version. - Version = "10.0.2" + Version = "10.1.0" ) // CheckForUpdates checks for any available updates diff --git a/core/router/api_builder.go b/core/router/api_builder.go index eed18a56..434e0474 100644 --- a/core/router/api_builder.go +++ b/core/router/api_builder.go @@ -709,7 +709,7 @@ func (api *APIBuilder) OnErrorCode(statusCode int, handlers ...context.Handler) // OnAnyErrorCode registers a handler which called when error status code written. // Same as `OnErrorCode` but registers all http error codes based on the `context.StatusCodeNotSuccessful` -// which defaults to < 200 || >= 400 for an error code, any previos error code will be overriden, +// which defaults to < 200 || >= 400 for an error code, any previous error code will be overridden, // so call it first if you want to use any custom handler for a specific error status code. // // Read more at: http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml