diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 503ff11fbf..07d8b199b0 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -8,4 +8,4 @@ Love iris? Please consider supporting the project: 👉 https://iris-go.com/donate Care to be part of a larger community? Fill our user experience form: -👉 https://goo.gl/forms/lnRbVgA6ICTkPyk02 \ No newline at end of file +👉 https://goo.gl/forms/lnRbVgA6ICTkPyk02 diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 3e708e67d5..f0baa85545 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -2,4 +2,4 @@ Read how you can [contribute to the project](https://github.com/kataras/iris/blob/master/CONTRIBUTING.md). -> Please attach an [issue](https://github.com/kataras/iris/issues) link which your PR solves otherwise your work may be rejected. \ No newline at end of file +> Please attach an [issue](https://github.com/kataras/iris/issues) link which your PR solves otherwise your work may be rejected. diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index ae441369fd..3c1c0dc486 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -17,10 +17,10 @@ jobs: restore-keys: | ${{ runner.os }}-go- - - name: Set up Go 1.13 + - name: Set up Go 1.14 uses: actions/setup-go@v1 with: - go-version: 1.13 + go-version: 1.14 id: go - name: Check out code into the Go module directory diff --git a/_examples/mvc/authenticated-controller/main.go b/_examples/mvc/authenticated-controller/main.go index 5aaba1f0f7..5c66eb0bd6 100644 --- a/_examples/mvc/authenticated-controller/main.go +++ b/_examples/mvc/authenticated-controller/main.go @@ -26,6 +26,11 @@ func main() { userRouter := app.Party("/user") { + // Use that in order to be able to register a route twice, + // last one will be executed if the previous route's handler(s) stopped and the response can be reset-ed. + // See core/router/route_register_rule_test.go#TestRegisterRuleOverlap. + userRouter.SetRegisterRule(iris.RouteOverlap) + // Initialize a new MVC application on top of the "userRouter". userApp := mvc.New(userRouter) // Register Dependencies. @@ -34,6 +39,7 @@ func main() { // Register Controllers. userApp.Handle(new(MeController)) userApp.Handle(new(UserController)) + userApp.Handle(new(UnauthenticatedUserController)) } // Open a client, e.g. Postman and visit the below endpoints. @@ -61,6 +67,15 @@ func authDependency(ctx iris.Context, session *sessions.Session) Authenticated { return Authenticated(userID) } +// UnauthenticatedUserController serves the "public" Unauthorized User API. +type UnauthenticatedUserController struct{} + +// GetMe registers a route that will be executed when authentication is not passed +// (see UserController.GetMe) too. +func (c *UnauthenticatedUserController) GetMe() string { + return `custom action to redirect on authentication page` +} + // UserController serves the "public" User API. type UserController struct { Session *sessions.Session diff --git a/context/context.go b/context/context.go index 8bbf4b7994..d39e2dba42 100644 --- a/context/context.go +++ b/context/context.go @@ -169,7 +169,6 @@ type Context interface { SetHandlers(Handlers) // Handlers keeps tracking of the current handlers. Handlers() Handlers - // HandlerIndex sets the current index of the // current context's handlers chain. // If n < 0 or the current handlers length is 0 then it just returns the diff --git a/context/gzip_response_writer.go b/context/gzip_response_writer.go index fe2aa8a051..ccca889ecd 100644 --- a/context/gzip_response_writer.go +++ b/context/gzip_response_writer.go @@ -192,6 +192,7 @@ func (w *GzipResponseWriter) Body() []byte { } // ResetBody resets the response body. +// Implements the `ResponseWriterBodyReseter`. func (w *GzipResponseWriter) ResetBody() { w.chunks = w.chunks[0:0] } @@ -202,6 +203,26 @@ func (w *GzipResponseWriter) Disable() { w.disabled = true } +// Reset disables the gzip content writer, clears headers, sets the status code to 200 +// and clears the cached body. +// +// Implements the `ResponseWriterReseter`. +func (w *GzipResponseWriter) Reset() bool { + // disable gzip content writer. + w.Disable() + // clear headers. + h := w.ResponseWriter.Header() + for k := range h { + h[k] = nil + } + // restore status code. + w.WriteHeader(defaultStatusCode) + // reset body. + w.ResetBody() + + return true +} + // FlushResponse validates the response headers in order to be compatible with the gzip written data // and writes the data to the underline ResponseWriter. func (w *GzipResponseWriter) FlushResponse() { diff --git a/context/response_recorder.go b/context/response_recorder.go index a71a56e980..765bfea2ec 100644 --- a/context/response_recorder.go +++ b/context/response_recorder.go @@ -139,11 +139,15 @@ func (w *ResponseRecorder) ClearHeaders() { } } -// Reset resets the response body, headers and the status code header. -func (w *ResponseRecorder) Reset() { +// Reset clears headers, sets the status code to 200 +// and clears the cached body. +// +// Implements the `ResponseWriterReseter`. +func (w *ResponseRecorder) Reset() bool { w.ClearHeaders() w.WriteHeader(defaultStatusCode) w.ResetBody() + return true } // FlushResponse the full body, headers and status code to the underline response writer diff --git a/context/response_writer.go b/context/response_writer.go index 00bcf9dc3e..dbc7d12d9b 100644 --- a/context/response_writer.go +++ b/context/response_writer.go @@ -105,6 +105,32 @@ type ResponseWriter interface { CloseNotifier() (http.CloseNotifier, bool) } +// ResponseWriterBodyReseter can be implemented by +// response writers that supports response body overriding +// (e.g. recorder and compressed). +type ResponseWriterBodyReseter interface { + // ResetBody should reset the body and reports back if it could reset successfully. + ResetBody() +} + +// ResponseWriterDisabler can be implemented +// by response writers that can be disabled and restored to their previous state +// (e.g. compressed). +type ResponseWriterDisabler interface { + // Disable should disable this type of response writer and fallback to the default one. + Disable() +} + +// ResponseWriterReseter can be implemented +// by response writers that can clear the whole response +// so a new handler can write into this from the beginning. +// E.g. recorder, compressed (full) and common writer (status code and headers). +type ResponseWriterReseter interface { + // Reset should reset the whole response and reports + // whether it could reset successfully. + Reset() bool +} + // +------------------------------------------------------------+ // | Response Writer Implementation | // +------------------------------------------------------------+ @@ -167,6 +193,25 @@ func (w *responseWriter) EndResponse() { releaseResponseWriter(w) } +// Reset clears headers, sets the status code to 200 +// and clears the cached body. +// +// Implements the `ResponseWriterReseter`. +func (w *responseWriter) Reset() bool { + if w.written > 0 { + return false // if already written we can't reset this type of response writer. + } + + h := w.Header() + for k := range h { + h[k] = nil + } + + w.written = NoWritten + w.statusCode = defaultStatusCode + return true +} + // SetWritten sets manually a value for written, it can be // NoWritten(-1) or StatusCodeWritten(0), > 0 means body length which is useless here. func (w *responseWriter) SetWritten(n int) { diff --git a/core/handlerconv/from_std.go b/core/handlerconv/from_std.go index dbdfa1c3d9..bc8aff2b30 100644 --- a/core/handlerconv/from_std.go +++ b/core/handlerconv/from_std.go @@ -18,21 +18,19 @@ func FromStd(handler interface{}) context.Handler { case context.Handler: { // - // it's already a iris handler + // it's already an Iris Handler // return h } - case http.Handler: - // - // handlerFunc.ServeHTTP(w,r) - // { + // + // handlerFunc.ServeHTTP(w,r) + // return func(ctx context.Context) { h.ServeHTTP(ctx.ResponseWriter(), ctx.Request()) } } - case func(http.ResponseWriter, *http.Request): { // @@ -40,7 +38,6 @@ func FromStd(handler interface{}) context.Handler { // return FromStd(http.HandlerFunc(h)) } - case func(http.ResponseWriter, *http.Request, http.HandlerFunc): { // @@ -48,7 +45,6 @@ func FromStd(handler interface{}) context.Handler { // return FromStdWithNext(h) } - default: { // @@ -60,9 +56,8 @@ func FromStd(handler interface{}) context.Handler { - func(w http.ResponseWriter, r *http.Request) - func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) --------------------------------------------------------------------- - It seems to be a %T points to: %v`, handler, handler)) + It seems to be a %T points to: %v`, handler, handler)) } - } } @@ -70,10 +65,10 @@ func FromStd(handler interface{}) context.Handler { // compatible context.Handler wrapper. func FromStdWithNext(h func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc)) context.Handler { return func(ctx context.Context) { - next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + next := func(w http.ResponseWriter, r *http.Request) { ctx.ResetRequest(r) ctx.Next() - }) + } h(ctx.ResponseWriter(), ctx.Request(), next) } diff --git a/core/router/api_builder.go b/core/router/api_builder.go index ce54681065..0d7f4b1609 100644 --- a/core/router/api_builder.go +++ b/core/router/api_builder.go @@ -91,6 +91,9 @@ func (repo *repository) register(route *Route, rule RouteRegisterRule) (*Route, return r, nil } else if rule == RouteError { return nil, fmt.Errorf("new route: %s conflicts with an already registered one: %s route", route.String(), r.String()) + } else if rule == RouteOverlap { + overlapRoute(r, route) + return route, nil } else { // replace existing with the latest one, the default behavior. repo.routes = append(repo.routes[:i], repo.routes[i+1:]...) @@ -113,6 +116,38 @@ func (repo *repository) register(route *Route, rule RouteRegisterRule) (*Route, return route, nil } +var defaultOverlapFilter = func(ctx context.Context) bool { + if ctx.IsStopped() { + // It's stopped and the response can be overriden by a new handler. + rs, ok := ctx.ResponseWriter().(context.ResponseWriterReseter) + return ok && rs.Reset() + } + + // It's not stopped, all OK no need to execute the alternative route. + return false +} + +func overlapRoute(r *Route, next *Route) { + next.BuildHandlers() + nextHandlers := next.Handlers[0:] + + decisionHandler := func(ctx context.Context) { + ctx.Next() + + if !defaultOverlapFilter(ctx) { + return + } + + ctx.SetCurrentRoute(next.ReadOnly) + ctx.HandlerIndex(0) + ctx.Do(nextHandlers) + } + + // NOTE(@kataras): Any UseGlobal call will prepend to this, if they are + // in the same Party then it's expected, otherwise not. + r.beginHandlers = append(context.Handlers{decisionHandler}, r.beginHandlers...) +} + // APIBuilder the visible API for constructing the router // and child routers. type APIBuilder struct { @@ -261,10 +296,17 @@ const ( // RouteError log when a route already exists, shown after the `Build` state, // server never starts. RouteError + // RouteOverlap will overlap the new route to the previous one. + // If the route stopped and its response can be reset then the new route will be execute. + RouteOverlap ) // SetRegisterRule sets a `RouteRegisterRule` for this Party and its children. -// Available values are: RouteOverride (the default one), RouteSkip and RouteError. +// Available values are: +// * RouteOverride (the default one) +// * RouteSkip +// * RouteError +// * RouteOverlap. func (api *APIBuilder) SetRegisterRule(rule RouteRegisterRule) Party { api.routeRegisterRule = rule return api diff --git a/core/router/party.go b/core/router/party.go index b553aa93a0..8efb92dfe7 100644 --- a/core/router/party.go +++ b/core/router/party.go @@ -116,7 +116,11 @@ type Party interface { // Example: https://github.com/kataras/iris/tree/master/_examples/mvc/middleware/without-ctx-next SetExecutionRules(executionRules ExecutionRules) Party // SetRegisterRule sets a `RouteRegisterRule` for this Party and its children. - // Available values are: RouteOverride (the default one), RouteSkip and RouteError. + // Available values are: + // * RouteOverride (the default one) + // * RouteSkip + // * RouteError + // * RouteOverlap. SetRegisterRule(rule RouteRegisterRule) Party // Handle registers a route to the server's router. diff --git a/core/router/route.go b/core/router/route.go index ec377ccea4..ac5dc9f2a8 100644 --- a/core/router/route.go +++ b/core/router/route.go @@ -66,6 +66,9 @@ type Route struct { // ReadOnly is the read-only structure of the Route. ReadOnly context.RouteReadOnly + + // OnBuild runs right before BuildHandlers. + OnBuild func(r *Route) } // NewRoute returns a new route based on its method, @@ -186,6 +189,10 @@ func (r *Route) RestoreStatus() bool { // at the `Application#Build` state. Do not call it manually, unless // you were defined your own request mux handler. func (r *Route) BuildHandlers() { + if r.OnBuild != nil { + r.OnBuild(r) + } + if len(r.beginHandlers) > 0 { r.Handlers = append(r.beginHandlers, r.Handlers...) r.beginHandlers = r.beginHandlers[0:0] diff --git a/core/router/route_register_rule_test.go b/core/router/route_register_rule_test.go index a0fbb65597..1a581ac24f 100644 --- a/core/router/route_register_rule_test.go +++ b/core/router/route_register_rule_test.go @@ -58,3 +58,54 @@ func testRegisterRule(e *httptest.Expect, expectedGetBody string) { } } } + +func TestRegisterRuleOverlap(t *testing.T) { + app := iris.New() + // TODO(@kataras) the overlapping does not work per-party yet, + // it just checks compares from the total app's routes (which is the best possible action to do + // because MVC applications can be separated into different parties too?). + usersRouter := app.Party("/users") + usersRouter.SetRegisterRule(iris.RouteOverlap) + + // second handler will be executed, status will be reset-ed as well, + // stop without data written. + usersRouter.Get("/", func(ctx iris.Context) { + ctx.StopWithStatus(iris.StatusUnauthorized) + }) + usersRouter.Get("/", func(ctx iris.Context) { + ctx.WriteString("data") + }) + + // first handler will be executed, no stop called. + usersRouter.Get("/p1", func(ctx iris.Context) { + ctx.StatusCode(iris.StatusUnauthorized) + }) + usersRouter.Get("/p1", func(ctx iris.Context) { + ctx.WriteString("not written") + }) + + // first handler will be executed, stop but with data sent on default writer + // (body sent cannot be reset-ed here). + usersRouter.Get("/p2", func(ctx iris.Context) { + ctx.StopWithText(iris.StatusUnauthorized, "no access") + }) + usersRouter.Get("/p2", func(ctx iris.Context) { + ctx.WriteString("not written") + }) + + // second will be executed, response can be reset-ed on recording. + usersRouter.Get("/p3", func(ctx iris.Context) { + ctx.Record() + ctx.StopWithText(iris.StatusUnauthorized, "no access") + }) + usersRouter.Get("/p3", func(ctx iris.Context) { + ctx.WriteString("p3 data") + }) + + e := httptest.New(t, app) + + e.GET("/users").Expect().Status(httptest.StatusOK).Body().Equal("data") + e.GET("/users/p1").Expect().Status(httptest.StatusUnauthorized).Body().Equal("Unauthorized") + e.GET("/users/p2").Expect().Status(httptest.StatusUnauthorized).Body().Equal("no access") + e.GET("/users/p3").Expect().Status(httptest.StatusOK).Body().Equal("p3 data") +} diff --git a/doc.go b/doc.go index bf97c9379f..db3ee4b9bd 100644 --- a/doc.go +++ b/doc.go @@ -38,7 +38,7 @@ Source code and other details for the project are available at GitHub: Current Version -12.1.8 +12.2.0 Installation diff --git a/go.mod b/go.mod index aa52fcad4e..60b9cf89a0 100644 --- a/go.mod +++ b/go.mod @@ -19,7 +19,7 @@ require ( github.com/iris-contrib/jade v1.1.4 github.com/iris-contrib/pongo2 v0.0.1 github.com/iris-contrib/schema v0.0.1 - github.com/json-iterator/go v1.1.9 + github.com/json-iterator/go v1.1.10 github.com/kataras/golog v0.0.18 github.com/kataras/neffos v0.0.16 github.com/kataras/pio v0.0.8 @@ -34,7 +34,7 @@ require ( github.com/vmihailenco/msgpack/v5 v5.0.0-alpha.2 go.etcd.io/bbolt v1.3.4 golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37 - golang.org/x/net v0.0.0-20200506145744-7e3656a0809f + golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7 golang.org/x/text v0.3.2 golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 gopkg.in/ini.v1 v1.57.0 diff --git a/iris.go b/iris.go index 0ef1555581..27a05a6fb9 100644 --- a/iris.go +++ b/iris.go @@ -617,6 +617,9 @@ const ( // RouteError log when a route already exists, shown after the `Build` state, // server never starts. RouteError = router.RouteError + // RouteOverlap will overlap the new route to the previous one. + // If the route stopped and its response can be reset then the new route will be execute. + RouteOverlap = router.RouteOverlap ) // Contains the enum values of the `Context.GetReferrer()` method,