diff --git a/HISTORY.md b/HISTORY.md index 191f37bf..c11ebb42 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -28,6 +28,9 @@ The codebase for Dependency Injection, Internationalization and localization and ## Fixes and Improvements +- A `Context.Logout` method is added, can be used to invalidate [basicauth](https://github.com/kataras/iris/blob/master/_examples/auth/basicauth/main.go) client credentials. +- Add the ability to [share functions](https://github.com/kataras/iris/tree/master/_examples/routing/writing-a-middleware/share-funcs) between handlers chain and add an [example](https://github.com/kataras/iris/tree/master/_examples/routing/writing-a-middleware/share-services) on sharing Go structures (aka services). + - Add the new `Party.UseOnce` method to the `*Route` - Add a new `*Route.RemoveHandler(interface{}) int`, deletes a handler from begin, main and done handlers based on its name or the handler pc function. Returns the total amount of handlers removed. @@ -306,6 +309,8 @@ var dirOpts = iris.DirOptions{ ## New Context Methods +- `Context.SetLogoutFunc(fn interface{}, persistenceArgs ...interface{})` and `Logout(args ...interface{}) error` methods to allow different kind of auth middlewares to be able to set a "logout" a user/client feature with a single function, the route handler may not be aware of the implementation of the authentication used. +- `Context.SetFunc(name string, fn interface{}, persistenceArgs ...interface{})` and `Context.CallFunc(name string, args ...interface{}) ([]reflect.Value, error)` to allow middlewares to share functions dynamically when the type of the function is not predictable, see the [example](https://github.com/kataras/iris/tree/master/_examples/routing/writing-a-middleware/share-funcs) for more. - `Context.TextYAML(interface{}) error` same as `Context.YAML` but with set the Content-Type to `text/yaml` instead (Google Chrome renders it as text). - `Context.IsDebug() bool` reports whether the application is running under debug/development mode. It is a shortcut of Application.Logger().Level >= golog.DebugLevel. - `Context.IsRecovered() bool` reports whether the current request was recovered from the [recover middleware](https://github.com/kataras/iris/tree/master/middleware/recover). Also the `iris.IsErrPrivate` function and `iris.ErrPrivate` interface have been introduced. diff --git a/README.md b/README.md index 073b3061..d764dbf4 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ With your help, we can improve Open Source web development for everyone! > Donations from **China** are now accepted!

+ Jasper Simranjit Singh Christopher Lamm 叶峻峣 diff --git a/_examples/README.md b/_examples/README.md index d6e23913..314d254c 100644 --- a/_examples/README.md +++ b/_examples/README.md @@ -48,6 +48,9 @@ * Middleware * [Per Route](routing/writing-a-middleware/per-route/main.go) * [Globally](routing/writing-a-middleware/globally/main.go) + * Share Values + * [Share Services](routing/writing-a-middleware/share-services/main.go) + * [Share Functions](routing/writing-a-middleware/share-funcs/main.go) * [Handlers Execution Rule](routing/route-handlers-execution-rules/main.go) * [Route Register Rule](routing/route-register-rule/main.go) * Convert net/http Handlers diff --git a/_examples/auth/basicauth/main.go b/_examples/auth/basicauth/main.go index cfc279f0..2486efb2 100644 --- a/_examples/auth/basicauth/main.go +++ b/_examples/auth/basicauth/main.go @@ -37,6 +37,8 @@ func newApp() *iris.Application { // http://localhost:8080/admin/settings needAuth.Get("/settings", h) + + needAuth.Get("/logout", logout) } return app @@ -55,3 +57,11 @@ func h(ctx iris.Context) { ctx.Writef("%s %s:%s", ctx.Path(), username, password) } + +func logout(ctx iris.Context) { + err := ctx.Logout() // fires 401, invalidates the basic auth. + if err != nil { + ctx.Application().Logger().Errorf("Logout error: %v", err) + } + ctx.Redirect("/admin", iris.StatusTemporaryRedirect) +} diff --git a/_examples/routing/writing-a-middleware/share-funcs/main.go b/_examples/routing/writing-a-middleware/share-funcs/main.go new file mode 100644 index 00000000..0b9be30d --- /dev/null +++ b/_examples/routing/writing-a-middleware/share-funcs/main.go @@ -0,0 +1,109 @@ +// Package main shows how you can share a +// function between handlers of the same chain. +// Note that, this case is very rarely used and it exists, +// mostly, for 3rd-party middleware creators. +// +// The middleware creator registers a dynamic function by Context.SetFunc and +// the route handler just needs to call Context.CallFunc(funcName, arguments), +// without knowning what is the specific middleware's implementation or who was the creator +// of that function, it may be a basicauth middleware's logout or session's logout. +// +// See Context.SetLogoutFunc and Context.Logout methods too (these are not covered here). +package main + +import ( + "fmt" + + "github.com/kataras/iris/v12" +) + +func main() { + app := newApp() + + // GET: http://localhost:8080 + app.Listen(":8080") +} + +func newApp() *iris.Application { + app := iris.New() + app.Use(middleware) + // OR app.UseRouter(middleware) + // to register it everywhere, + // including the HTTP errors. + + app.Get("/", handler) + app.Get("/2", middleware2, handler2) + app.Get("/3", middleware3, handler3) + + return app +} + +// Assume: this is a middleware which does not export +// the 'hello' function for several reasons +// but we offer a 'greeting' optional feature to the route handler. +func middleware(ctx iris.Context) { + ctx.SetFunc("greet", hello) + ctx.Next() +} + +// Assume: this is a handler which needs to "greet" the client but +// the function for that job is not predictable, +// it may change - dynamically (SetFunc) - depending on +// the middlewares registered before this route handler. +// E.g. it may be a "Hello $name" or "Greetings $Name". +func handler(ctx iris.Context) { + outputs, err := ctx.CallFunc("greet", "Gophers") + if err != nil { + ctx.StopWithError(iris.StatusInternalServerError, err) + return + } + + response := outputs[0].Interface().(string) + ctx.WriteString(response) +} + +func middleware2(ctx iris.Context) { + ctx.SetFunc("greet", sayHello) + ctx.Next() +} + +func handler2(ctx iris.Context) { + _, err := ctx.CallFunc("greet", "Gophers [2]") + if err != nil { + ctx.StopWithError(iris.StatusInternalServerError, err) + return + } +} + +func middleware3(ctx iris.Context) { + ctx.SetFunc("job", function3) + ctx.Next() +} + +func handler3(ctx iris.Context) { + _, err := ctx.CallFunc("job") + if err != nil { + ctx.StopWithError(iris.StatusInternalServerError, err) + return + } + + ctx.WriteString("OK, job was executed.\nSee the command prompt.") +} + +/* +| ------------------------ | +| function implementations | +| ------------------------ | +*/ + +func hello(name string) string { + return fmt.Sprintf("Hello, %s!", name) +} + +func sayHello(ctx iris.Context, name string) { + ctx.WriteString(hello(name)) +} + +func function3() { + fmt.Printf("function3 called\n") +} diff --git a/_examples/routing/writing-a-middleware/share-funcs/main_test.go b/_examples/routing/writing-a-middleware/share-funcs/main_test.go new file mode 100644 index 00000000..a6057d93 --- /dev/null +++ b/_examples/routing/writing-a-middleware/share-funcs/main_test.go @@ -0,0 +1,16 @@ +package main + +import ( + "testing" + + "github.com/kataras/iris/v12/httptest" +) + +func TestShareFuncs(t *testing.T) { + app := newApp() + e := httptest.New(t, app) + + e.GET("/").Expect().Status(httptest.StatusOK).Body().Equal("Hello, Gophers!") + e.GET("/2").Expect().Status(httptest.StatusOK).Body().Equal("Hello, Gophers [2]!") + e.GET("/3").Expect().Status(httptest.StatusOK).Body().Equal("OK, job was executed.\nSee the command prompt.") +} diff --git a/_examples/routing/writing-a-middleware/share-services/main.go b/_examples/routing/writing-a-middleware/share-services/main.go new file mode 100644 index 00000000..9deab8a8 --- /dev/null +++ b/_examples/routing/writing-a-middleware/share-services/main.go @@ -0,0 +1,85 @@ +package main + +import ( + "fmt" + + "github.com/kataras/iris/v12" +) + +func main() { + app := newApp() + + // GET: http://localhost:8080 + app.Listen(":8080") +} + +func newApp() *iris.Application { + app := iris.New() + app.Use(middleware) + // OR app.UseRouter(middleware) + // to register it everywhere, + // including the HTTP errors. + + app.Get("/", handler) + + return app +} + +func middleware(ctx iris.Context) { + service := &helloService{ + Greeting: "Hello", + } + setService(ctx, service) + + ctx.Next() +} + +func handler(ctx iris.Context) { + service, ok := getService(ctx) + if !ok { + ctx.StopWithStatus(iris.StatusInternalServerError) + return + } + + response := service.Greet("Gophers") + ctx.WriteString(response) +} + +/* +| ---------------------- | +| service implementation | +| ---------------------- | +*/ + +const serviceContextKey = "app.service" + +func setService(ctx iris.Context, service GreetService) { + ctx.Values().Set(serviceContextKey, service) +} + +func getService(ctx iris.Context) (GreetService, bool) { + v := ctx.Values().Get(serviceContextKey) + if v == nil { + return nil, false + } + + service, ok := v.(GreetService) + if !ok { + return nil, false + } + + return service, true +} + +// A GreetService example. +type GreetService interface { + Greet(name string) string +} + +type helloService struct { + Greeting string +} + +func (m *helloService) Greet(name string) string { + return fmt.Sprintf("%s, %s!", m.Greeting, name) +} diff --git a/_examples/routing/writing-a-middleware/share-services/main_test.go b/_examples/routing/writing-a-middleware/share-services/main_test.go new file mode 100644 index 00000000..a5b5e137 --- /dev/null +++ b/_examples/routing/writing-a-middleware/share-services/main_test.go @@ -0,0 +1,14 @@ +package main + +import ( + "testing" + + "github.com/kataras/iris/v12/httptest" +) + +func TestShareServices(t *testing.T) { + app := newApp() + e := httptest.New(t, app) + + e.GET("/").Expect().Status(httptest.StatusOK).Body().Equal("Hello, Gophers!") +} diff --git a/_examples/sessions/database/redis/main.go b/_examples/sessions/database/redis/main.go index 2eb22e07..63860e1b 100644 --- a/_examples/sessions/database/redis/main.go +++ b/_examples/sessions/database/redis/main.go @@ -26,7 +26,7 @@ func main() { Password: "", Database: "", Prefix: "myapp-", - Driver: redis.GoRedis(), // defautls. + Driver: redis.GoRedis(), // defaults. }) // Optionally configure the underline driver: diff --git a/context/context.go b/context/context.go index b9af9e3c..efe354ac 100644 --- a/context/context.go +++ b/context/context.go @@ -5162,6 +5162,97 @@ func (ctx *Context) IsRecovered() (*ErrPanicRecovery, bool) { return nil, false } +const ( + funcsContextPrefixKey = "iris.funcs." + funcLogoutContextKey = "auth.logout_func" +) + +// SetFunc registers a custom function to this Request. +// It's a helper to pass dynamic functions across handlers of the same chain. +// For a more complete solution please use Dependency Injection instead. +// This is just an easy to way to pass a function to the +// next handler like ctx.Values().Set/Get does. +// Sometimes is faster and easier to pass the object as a request value +// and cast it when you want to use one of its methods instead of using +// these `SetFunc` and `CallFunc` methods. +// This implementation is suitable for functions that may change inside the +// handler chain and the object holding the method is not predictable. +// +// The "name" argument is the custom name of the function, +// you will use its name to call it later on, e.g. "auth.myfunc". +// +// The second, "fn" argument is the raw function/method you want +// to pass through the next handler(s) of the chain. +// +// The last variadic input argument is optionally, if set +// then its arguments are passing into the function's input arguments, +// they should be always be the first ones to be accepted by the "fn" inputs, +// e.g. an object, a receiver or a static service. +// +// See its `CallFunc` to call the "fn" on the next handler. +// +// Example at: +// https://github.com/kataras/iris/tree/master/_examples/routing/writing-a-middleware/share-funcs +func (ctx *Context) SetFunc(name string, fn interface{}, persistenceArgs ...interface{}) { + f := newFunc(name, fn, persistenceArgs...) + ctx.values.Set(funcsContextPrefixKey+name, f) +} + +// GetFunc returns the context function declaration which holds +// some information about the function registered under the given "name" by +// the `SetFunc` method. +func (ctx *Context) GetFunc(name string) (*Func, bool) { + fn := ctx.values.Get(funcsContextPrefixKey + name) + if fn == nil { + return nil, false + } + + return fn.(*Func), true +} + +// CallFunc calls the function registered by the `SetFunc`. +// The input arguments MUST match the expected ones. +// +// If the registered function was just a handler +// or a handler which returns an error +// or a simple function +// or a simple function which returns an error +// then this operation will perform without any serious cost, +// otherwise reflection will be used instead, which may slow down the overall performance. +// +// Retruns ErrNotFound if the function was not registered. +// +// For a more complete solution without limiations navigate through +// the Iris Dependency Injection feature instead. +func (ctx *Context) CallFunc(name string, args ...interface{}) ([]reflect.Value, error) { + fn, ok := ctx.GetFunc(name) + if !ok || fn == nil { + return nil, ErrNotFound + } + + return fn.call(ctx, args...) +} + +// SetLogoutFunc registers a custom logout function that will be +// available to use inside the next handler(s). The function +// may be registered multiple times but the last one is the valid. +// So a logout function may start with basic authentication +// and other middleware in the chain may change it to a custom sessions logout one. +// This method uses the `SetFunc` method under the hoods. +// +// See `Logout` method too. +func (ctx *Context) SetLogoutFunc(fn interface{}, persistenceArgs ...interface{}) { + ctx.SetFunc(funcLogoutContextKey, fn, persistenceArgs...) +} + +// Logout calls the registered logout function. +// Returns ErrNotFound if a logout function was not specified +// by a prior call of `SetLogoutFunc`. +func (ctx *Context) Logout(args ...interface{}) error { + _, err := ctx.CallFunc(funcLogoutContextKey, args...) + return err +} + const idContextKey = "iris.context.id" // SetID sets an ID, any value, to the Request Context. diff --git a/context/context_func.go b/context/context_func.go new file mode 100644 index 00000000..177ce1ec --- /dev/null +++ b/context/context_func.go @@ -0,0 +1,203 @@ +package context + +import ( + "errors" + "reflect" + "sync" +) + +// ErrInvalidArgs fires when the `Context.CallFunc` +// is called with invalid number of arguments. +var ErrInvalidArgs = errors.New("invalid arguments") + +// Func represents a function registered by the Context. +// See its `buildMeta` and `call` internal methods. +type Func struct { + RegisterName string // the name of which this function is registered, for information only. + Raw interface{} // the Raw function, can be used for custom casting. + PersistenceArgs []interface{} // the persistence input arguments given on registration. + + once sync.Once // guards build once, on first call. + // Available after the first call. + Meta *FuncMeta +} + +func newFunc(name string, fn interface{}, persistenceArgs ...interface{}) *Func { + return &Func{ + RegisterName: name, + Raw: fn, + PersistenceArgs: persistenceArgs, + } +} + +// FuncMeta holds the necessary information about a registered +// context function. Built once by the Func. +type FuncMeta struct { + Handler Handler // when it's just a handler. + HandlerWithErr func(*Context) error // when it's just a handler which returns an error. + RawFunc func() // when it's just a func. + RawFuncWithErr func() error // when it's just a func which returns an error. + RawFuncArgs func(...interface{}) + RawFuncArgsWithErr func(...interface{}) error + + Value reflect.Value + Type reflect.Type + ExpectedArgumentsLength int + PersistenceInputs []reflect.Value + AcceptsContext bool // the Context, if exists should be always first argument. + ReturnsError bool // when the function's last output argument is error. +} + +func (f *Func) buildMeta() { + switch fn := f.Raw.(type) { + case Handler: + f.Meta = &FuncMeta{Handler: fn} + return + case func(*Context): + f.Meta = &FuncMeta{Handler: fn} + return + case func(*Context) error: + f.Meta = &FuncMeta{HandlerWithErr: fn} + return + case func(): + f.Meta = &FuncMeta{RawFunc: fn} + return + case func() error: + f.Meta = &FuncMeta{RawFuncWithErr: fn} + return + case func(...interface{}): + f.Meta = &FuncMeta{RawFuncArgs: fn} + return + case func(...interface{}) error: + f.Meta = &FuncMeta{RawFuncArgsWithErr: fn} + return + } + + fn := f.Raw + + meta := FuncMeta{} + if val, ok := fn.(reflect.Value); ok { + meta.Value = val + } else { + meta.Value = reflect.ValueOf(fn) + } + + meta.Type = meta.Value.Type() + + if meta.Type.Kind() != reflect.Func { + return + } + + meta.ExpectedArgumentsLength = meta.Type.NumIn() + + skipInputs := len(meta.PersistenceInputs) + if meta.ExpectedArgumentsLength > skipInputs { + meta.AcceptsContext = isContext(meta.Type.In(skipInputs)) + } + + if numOut := meta.Type.NumOut(); numOut > 0 { + // error should be the last output. + if isError(meta.Type.Out(numOut - 1)) { + meta.ReturnsError = true + } + } + + persistenceArgs := f.PersistenceArgs + if len(persistenceArgs) > 0 { + inputs := make([]reflect.Value, 0, len(persistenceArgs)) + for _, arg := range persistenceArgs { + if in, ok := arg.(reflect.Value); ok { + inputs = append(inputs, in) + } else { + inputs = append(inputs, reflect.ValueOf(in)) + } + } + + meta.PersistenceInputs = inputs + } + + f.Meta = &meta +} + +func (f *Func) call(ctx *Context, args ...interface{}) ([]reflect.Value, error) { + f.once.Do(f.buildMeta) + meta := f.Meta + + if meta.Handler != nil { + meta.Handler(ctx) + return nil, nil + } + + if meta.HandlerWithErr != nil { + return nil, meta.HandlerWithErr(ctx) + } + + if meta.RawFunc != nil { + meta.RawFunc() + return nil, nil + } + + if meta.RawFuncWithErr != nil { + return nil, meta.RawFuncWithErr() + } + + if meta.RawFuncArgs != nil { + meta.RawFuncArgs(args...) + return nil, nil + } + + if meta.RawFuncArgsWithErr != nil { + return nil, meta.RawFuncArgsWithErr(args...) + } + + inputs := make([]reflect.Value, 0, f.Meta.ExpectedArgumentsLength) + inputs = append(inputs, f.Meta.PersistenceInputs...) + if f.Meta.AcceptsContext { + inputs = append(inputs, reflect.ValueOf(ctx)) + } + + for _, arg := range args { + if in, ok := arg.(reflect.Value); ok { + inputs = append(inputs, in) + } else { + inputs = append(inputs, reflect.ValueOf(arg)) + } + } + + // keep it here, the inptus may contain the context. + if f.Meta.ExpectedArgumentsLength != len(inputs) { + return nil, ErrInvalidArgs + } + + outputs := f.Meta.Value.Call(inputs) + return outputs, getError(outputs) +} + +var contextType = reflect.TypeOf((*Context)(nil)) + +// isContext returns true if the "typ" is a type of Context. +func isContext(typ reflect.Type) bool { + return typ == contextType +} + +var errTyp = reflect.TypeOf((*error)(nil)).Elem() + +// isError returns true if "typ" is type of `error`. +func isError(typ reflect.Type) bool { + return typ.Implements(errTyp) +} + +func getError(outputs []reflect.Value) error { + if n := len(outputs); n > 0 { + lastOut := outputs[n-1] + if isError(lastOut.Type()) { + if lastOut.IsNil() { + return nil + } + + return lastOut.Interface().(error) + } + } + + return nil +} diff --git a/middleware/basicauth/basicauth.go b/middleware/basicauth/basicauth.go index fc90eba3..5b71ae8f 100644 --- a/middleware/basicauth/basicauth.go +++ b/middleware/basicauth/basicauth.go @@ -21,15 +21,15 @@ type ( HeaderValue string Username string logged bool + forceLogout bool // in order to be able to invalidate and use a redirect response. expires time.Time mu sync.RWMutex } - encodedUsers []*encodedUser basicAuthMiddleware struct { - config Config + config *Config // these are filled from the config.Users map at the startup - auth encodedUsers + auth []*encodedUser realmHeaderValue string // The below can be removed but they are here because on the future we may add dynamic options for those two fields, @@ -54,7 +54,7 @@ func New(c Config) context.Handler { config.Expires = c.Expires config.OnAsk = c.OnAsk - b := &basicAuthMiddleware{config: config} + b := &basicAuthMiddleware{config: &config} b.init() return b.Serve } @@ -71,7 +71,7 @@ func Default(users map[string]string) context.Handler { func (b *basicAuthMiddleware) init() { // pass the encoded users from the user's config's Users value - b.auth = make(encodedUsers, 0, len(b.config.Users)) + b.auth = make([]*encodedUser, 0, len(b.config.Users)) for k, v := range b.config.Users { fullUser := k + ":" + v @@ -109,12 +109,19 @@ func (b *basicAuthMiddleware) askForCredentials(ctx *context.Context) { // Serve the actual middleware func (b *basicAuthMiddleware) Serve(ctx *context.Context) { auth, found := b.findAuth(ctx.GetHeader("Authorization")) - if !found { + if !found || auth.forceLogout { + if auth != nil { + auth.mu.Lock() + auth.forceLogout = false + auth.mu.Unlock() + } + b.askForCredentials(ctx) ctx.StopExecution() return // don't continue to the next handler } + // all ok if b.expireEnabled { if !auth.logged { @@ -136,5 +143,26 @@ func (b *basicAuthMiddleware) Serve(ctx *context.Context) { return } } + + if !b.config.DisableLogoutFunc { + ctx.SetLogoutFunc(b.Logout) + } + ctx.Next() // continue } + +// Logout sends a 401 so the browser/client can invalidate the +// Basic Authentication and also sets the underline user's logged field to false, +// so its expiration resets when re-ask for credentials. +// +// End-developers should call the `Context.Logout()` method +// to fire this method as this structure is hidden. +func (b *basicAuthMiddleware) Logout(ctx *context.Context) { + ctx.StatusCode(401) + if auth, found := b.findAuth(ctx.GetHeader("Authorization")); found { + auth.mu.Lock() + auth.logged = false + auth.forceLogout = true + auth.mu.Unlock() + } +} diff --git a/middleware/basicauth/config.go b/middleware/basicauth/config.go index 2a4758c5..ddd76316 100644 --- a/middleware/basicauth/config.go +++ b/middleware/basicauth/config.go @@ -42,11 +42,14 @@ type Config struct { // // Defaults to nil. OnAsk context.Handler + + // DisableLogoutFunc disables the registration of the custom basicauth Context.Logout. + DisableLogoutFunc bool } // DefaultConfig returns the default configs for the BasicAuth middleware func DefaultConfig() Config { - return Config{make(map[string]string), DefaultBasicAuthRealm, 0, nil} + return Config{make(map[string]string), DefaultBasicAuthRealm, 0, nil, false} } // User returns the user from context key same as ctx.Request().BasicAuth().