From cf36063adfad01fac37a1c667c5140bf141824c3 Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Mon, 28 Mar 2022 14:00:26 +0300 Subject: [PATCH 01/20] first release of SSO package and more examples --- NOTICE | 4 + _examples/README.md | 5 +- _examples/auth/sso/README.md | 12 + _examples/auth/sso/main.go | 135 +++++ _examples/auth/sso/sso.yml | 32 + _examples/auth/sso/user.go | 33 + _examples/auth/sso/user_provider.go | 100 +++ _examples/auth/sso/views/layouts/main.html | 30 + _examples/auth/sso/views/partials/footer.html | 1 + _examples/auth/sso/views/signin.html | 9 + _examples/file-server/webdav/main.go | 57 ++ _examples/file-server/webdav/newdir/.gitkeep | 0 _examples/file-server/webdav/test.txt | 1 + _examples/mvc/grpc-compatible/main.go | 31 +- .../mvc/websocket-sso/browser/index.html | 106 ++++ _examples/mvc/websocket-sso/main.go | 72 +++ _examples/mvc/websocket-sso/sso.yml | 32 + _examples/mvc/websocket-sso/user.go | 33 + _examples/mvc/websocket-sso/user_provider.go | 100 +++ .../mvc/websocket-sso/views/layouts/main.html | 30 + .../websocket-sso/views/partials/footer.html | 1 + _examples/mvc/websocket-sso/views/signin.html | 9 + aliases.go | 43 +- context/context.go | 8 +- core/router/api_builder.go | 7 + go.mod | 3 +- go.sum | 6 +- sso/configuration.go | 162 +++++ sso/provider.go | 83 +++ sso/sso.go | 568 ++++++++++++++++++ sso/user.go | 53 ++ x/client/client_test.go | 35 +- x/errors/errors.go | 71 +-- 33 files changed, 1805 insertions(+), 67 deletions(-) create mode 100644 _examples/auth/sso/README.md create mode 100644 _examples/auth/sso/main.go create mode 100644 _examples/auth/sso/sso.yml create mode 100644 _examples/auth/sso/user.go create mode 100644 _examples/auth/sso/user_provider.go create mode 100644 _examples/auth/sso/views/layouts/main.html create mode 100644 _examples/auth/sso/views/partials/footer.html create mode 100644 _examples/auth/sso/views/signin.html create mode 100644 _examples/file-server/webdav/main.go create mode 100644 _examples/file-server/webdav/newdir/.gitkeep create mode 100644 _examples/file-server/webdav/test.txt create mode 100644 _examples/mvc/websocket-sso/browser/index.html create mode 100644 _examples/mvc/websocket-sso/main.go create mode 100644 _examples/mvc/websocket-sso/sso.yml create mode 100644 _examples/mvc/websocket-sso/user.go create mode 100644 _examples/mvc/websocket-sso/user_provider.go create mode 100644 _examples/mvc/websocket-sso/views/layouts/main.html create mode 100644 _examples/mvc/websocket-sso/views/partials/footer.html create mode 100644 _examples/mvc/websocket-sso/views/signin.html create mode 100644 sso/configuration.go create mode 100644 sso/provider.go create mode 100644 sso/sso.go create mode 100644 sso/user.go diff --git a/NOTICE b/NOTICE index 72189f00..44bfc0ff 100644 --- a/NOTICE +++ b/NOTICE @@ -44,6 +44,10 @@ Revision ID: 5fc50a00491616d5cd0cbce3abd8b699838e25ca easyjson 8ab5ff9cd8e4e43 https://github.com/mailru/easyjson 2e8b79f6c47d324 a31dd803cf + + securecookie e59506cc896acb7 https://github.com/gorilla/securecookie + f7bf732d4fdf5e2 + 5f7ccd8983 semver 4487282d78122a2 https://github.com/blang/semver 45e413d7515e7c5 16b70c33fd diff --git a/_examples/README.md b/_examples/README.md index 32be6859..78905cd2 100644 --- a/_examples/README.md +++ b/_examples/README.md @@ -124,6 +124,7 @@ * [Embedded Single Page Application with other routes](file-server/single-page-application/embedded-single-page-application-with-other-routes/main.go) * [Upload File](file-server/upload-file/main.go) * [Upload Multiple Files](file-server/upload-files/main.go) + * [WebDAV](file-server/webdav/main.go) * View * [Overview](view/overview/main.go) * [Layout](view/layout) @@ -212,7 +213,7 @@ * [Basic](i18n/basic) * [Ttemplates and Functions](i18n/template) * [Pluralization and Variables](i18n/plurals) -* Authentication, Authorization & Bot Detection +* Authentication, Authorization & Bot Detection * Basic Authentication * [Basic](auth/basicauth/basic) * [Load from a slice of Users](auth/basicauth/users_list) @@ -225,6 +226,7 @@ * [Blocklist](auth/jwt/blocklist/main.go) * [Refresh Token](auth/jwt/refresh-token/main.go) * [Tutorial](auth/jwt/tutorial) + * [SSO](auth/sso) **NEW (GO 1.18 Generics required)** * [JWT (community edition)](https://github.com/iris-contrib/middleware/tree/v12/jwt/_example/main.go) * [OAUth2](auth/goth/main.go) * [Manage Permissions](auth/permissions/main.go) @@ -277,6 +279,7 @@ * [Authenticated Controller](mvc/authenticated-controller/main.go) * [Versioned Controller](mvc/versioned-controller/main.go) * [Websocket Controller](mvc/websocket) + * [Websocket + Authentication (SSO)](mvc/websocket-sso) **NEW (GO 1.18 Generics required)** * [Register Middleware](mvc/middleware) * [gRPC](mvc/grpc-compatible) * [gRPC Bidirectional Stream](mvc/grpc-compatible-bidirectional-stream) diff --git a/_examples/auth/sso/README.md b/_examples/auth/sso/README.md new file mode 100644 index 00000000..ffa7a823 --- /dev/null +++ b/_examples/auth/sso/README.md @@ -0,0 +1,12 @@ +# SSO (Single Sign On) + +```sh +$ go run . +``` + +1. GET/POST: http://localhost:8080/signin +2. GET: http://localhost:8080/member +3. GET: http://localhost:8080/owner +4. POST: http://localhost:8080/refresh +5. GET: http://localhost:8080/signout +6. GET: http://localhost:8080/signout-all \ No newline at end of file diff --git a/_examples/auth/sso/main.go b/_examples/auth/sso/main.go new file mode 100644 index 00000000..f09236a2 --- /dev/null +++ b/_examples/auth/sso/main.go @@ -0,0 +1,135 @@ +//go:build go1.18 + +package main + +import ( + "fmt" + + "github.com/kataras/iris/v12" + "github.com/kataras/iris/v12/sso" +) + +func allowRole(role AccessRole) sso.TVerify[User] { + return func(u User) error { + if !u.Role.Allow(role) { + return fmt.Errorf("invalid role") + } + + return nil + } +} + +const configFilename = "./sso.yml" + +func main() { + app := iris.New() + app.RegisterView(iris.Blocks(iris.Dir("./views"), ".html"). + LayoutDir("layouts"). + Layout("main")) + + /* + // Easiest 1-liner way, load from configuration and initialize a new sso instance: + s := sso.MustLoad[User]("./sso.yml") + // Bind a configuration from file: + var c sso.Configuration + c.BindFile("./sso.yml") + s, err := sso.New[User](c) + // OR create new programmatically configuration: + config := sso.Configuration{ + ...fields + } + s, err := sso.New[User](config) + // OR generate a new configuration: + config := sso.MustGenerateConfiguration() + s, err := sso.New[User](config) + // OR generate a new config and save it if cannot open the config file. + if _, err := os.Stat(configFilename); err != nil { + generatedConfig := sso.MustGenerateConfiguration() + configContents, err := generatedConfig.ToYAML() + if err != nil { + panic(err) + } + + err = os.WriteFile(configFilename, configContents, 0600) + if err != nil { + panic(err) + } + } + */ + + // 1. Load configuration from a file. + ssoConfig, err := sso.LoadConfiguration(configFilename) + if err != nil { + panic(err) + } + + // 2. Initialize a new sso instance for "User" claims (generics: go1.18 +). + s, err := sso.New[User](ssoConfig) + if err != nil { + panic(err) + } + + // 3. Add a custom provider, in our case is just a memory-based one. + s.AddProvider(NewProvider()) + // 3.1. Optionally set a custom error handler. + // s.SetErrorHandler(new(sso.DefaultErrorHandler)) + + app.Get("/signin", renderSigninForm) + // 4. generate token pairs. + app.Post("/signin", s.SigninHandler) + // 5. refresh token pairs. + app.Post("/refresh", s.RefreshHandler) + // 6. calls the provider's InvalidateToken method. + app.Get("/signout", s.SignoutHandler) + // 7. calls the provider's InvalidateTokens method. + app.Get("/signout-all", s.SignoutAllHandler) + + // 8.1. allow access for users with "Member" role. + app.Get("/member", s.VerifyHandler(allowRole(Member)), renderMemberPage(s)) + // 8.2. allow access for users with "Owner" role. + app.Get("/owner", s.VerifyHandler(allowRole(Owner)), renderOwnerPage(s)) + + /* Subdomain user verify: + app.Subdomain("owner", s.VerifyHandler(allowRole(Owner))).Get("/", renderOwnerPage(s)) + */ + app.Listen(":8080", iris.WithOptimizations) // Setup HTTPS/TLS for production instead. + /* Test subdomain user verify, one way is ingrok, + add the below to the arguments above: + , iris.WithConfiguration(iris.Configuration{ + EnableOptmizations: true, + Tunneling: iris.TunnelingConfiguration{ + AuthToken: "YOUR_AUTH_TOKEN", + Region: "us", + Tunnels: []tunnel.Tunnel{ + { + Name: "Iris SSO (Test)", + Addr: ":8080", + Hostname: "YOUR_DOMAIN", + }, + { + Name: "Iris SSO (Test Subdomain)", + Addr: ":8080", + Hostname: "owner.YOUR_DOMAIN", + }, + }, + }, + })*/ +} + +func renderSigninForm(ctx iris.Context) { + ctx.View("signin", iris.Map{"Title": "Signin Page"}) +} + +func renderMemberPage(s *sso.SSO[User]) iris.Handler { + return func(ctx iris.Context) { + user := s.GetUser(ctx) + ctx.Writef("Hello member: %s\n", user.Email) + } +} + +func renderOwnerPage(s *sso.SSO[User]) iris.Handler { + return func(ctx iris.Context) { + user := s.GetUser(ctx) + ctx.Writef("Hello owner: %s\n", user.Email) + } +} diff --git a/_examples/auth/sso/sso.yml b/_examples/auth/sso/sso.yml new file mode 100644 index 00000000..c25a22bc --- /dev/null +++ b/_examples/auth/sso/sso.yml @@ -0,0 +1,32 @@ +Cookie: # optional. + Name: "iris_sso" + Hash: "D*G-KaPdSgUkXp2s5v8y/B?E(H+MbQeThWmYq3t6w9z$C&F)J@NcRfUjXn2r4u7x" # length of 64 characters (512-bit). + Block: "VkYp3s6v9y$B&E)H@McQfTjWmZq4t7w!" # length of 32 characters (256-bit). +Keys: + - ID: IRIS_SSO_ACCESS # required. + Alg: EdDSA + MaxAge: 2h # 2 hours lifetime for access tokens. + Private: |+ + -----BEGIN PRIVATE KEY----- + MC4CAQAwBQYDK2VwBCIEIFdZWoDdFny5SMnP9Fyfr8bafi/B527EVZh8JJjDTIFO + -----END PRIVATE KEY----- + Public: |+ + -----BEGIN PUBLIC KEY----- + MCowBQYDK2VwAyEAzpgjKSr9E032DX+foiOxq1QDsbzjLxagTN+yVpGWZB4= + -----END PUBLIC KEY----- + - ID: IRIS_SSO_REFRESH # optional. Good practise to have it though. + Alg: EdDSA + # 1 month lifetime for refresh tokens, + # after that period the user has to signin again. + MaxAge: 720h + Private: |+ + -----BEGIN PRIVATE KEY----- + MC4CAQAwBQYDK2VwBCIEIHJ1aoIjA2sRp5eqGjGR3/UMucrHbBdBv9p8uwfzZ1KZ + -----END PRIVATE KEY----- + Public: |+ + -----BEGIN PUBLIC KEY----- + MCowBQYDK2VwAyEAsKKAr+kDtfAqwG7cZdoEAfh9jHt9W8qi9ur5AA1KQAQ= + -----END PUBLIC KEY----- + # Example of setting a binary form of the encryption key for refresh tokens, + # it could be a "string" as well. + EncryptionKey: !!binary stSNLTu91YyihPxzeEOXKwGVMG00CjcC/68G8nMgmqA= diff --git a/_examples/auth/sso/user.go b/_examples/auth/sso/user.go new file mode 100644 index 00000000..a2e93017 --- /dev/null +++ b/_examples/auth/sso/user.go @@ -0,0 +1,33 @@ +//go:build go1.18 + +package main + +type AccessRole uint16 + +func (r AccessRole) Is(v AccessRole) bool { + return r&v != 0 +} + +func (r AccessRole) Allow(v AccessRole) bool { + return r&v >= v +} + +const ( + InvalidAccessRole AccessRole = 1 << iota + Read + Write + Delete + + Owner = Read | Write | Delete + Member = Read | Write +) + +type User struct { + ID string `json:"id"` + Email string `json:"email"` + Role AccessRole `json:"role"` +} + +func (u User) GetID() string { + return u.ID +} diff --git a/_examples/auth/sso/user_provider.go b/_examples/auth/sso/user_provider.go new file mode 100644 index 00000000..151e426b --- /dev/null +++ b/_examples/auth/sso/user_provider.go @@ -0,0 +1,100 @@ +//go:build go1.18 + +package main + +import ( + "context" + "fmt" + "sync" + "time" + + "github.com/kataras/iris/v12/sso" +) + +type Provider struct { + dataset []User + + invalidated map[string]struct{} // key = token. Entry is blocked. + invalidatedAll map[string]int64 // key = user id, value = timestamp. Issued before is consider invalid. + mu sync.RWMutex +} + +func NewProvider() *Provider { + return &Provider{ + dataset: []User{ + { + ID: "id-1", + Email: "kataras2006@hotmail.com", + Role: Owner, + }, + { + ID: "id-2", + Email: "example@example.com", + Role: Member, + }, + }, + invalidated: make(map[string]struct{}), + invalidatedAll: make(map[string]int64), + } +} + +func (p *Provider) Signin(ctx context.Context, username, password string) (User, error) { // fired on SigninHandler. + // your database... + for _, user := range p.dataset { + if user.Email == username { + return user, nil + } + } + + return User{}, fmt.Errorf("user not found") +} + +func (p *Provider) ValidateToken(ctx context.Context, standardClaims sso.StandardClaims, u User) error { // fired on VerifyHandler. + // your database and checks of blocked tokens... + + // check for specific token ids. + p.mu.RLock() + _, tokenBlocked := p.invalidated[standardClaims.ID] + if !tokenBlocked { + // this will disallow refresh tokens with origin jwt token id as the blocked access token as well. + if standardClaims.OriginID != "" { + _, tokenBlocked = p.invalidated[standardClaims.OriginID] + } + } + p.mu.RUnlock() + + if tokenBlocked { + return fmt.Errorf("token was invalidated") + } + // + + // check all tokens issuet before the "InvalidateToken" method was fired for this user. + p.mu.RLock() + ts, oldUserBlocked := p.invalidatedAll[u.ID] + p.mu.RUnlock() + + if oldUserBlocked && standardClaims.IssuedAt <= ts { + return fmt.Errorf("token was invalidated") + } + // + + return nil // else valid. +} + +func (p *Provider) InvalidateToken(ctx context.Context, standardClaims sso.StandardClaims, u User) error { // fired on SignoutHandler. + // invalidate this specific token. + p.mu.Lock() + p.invalidated[standardClaims.ID] = struct{}{} + p.mu.Unlock() + + return nil +} + +func (p *Provider) InvalidateTokens(ctx context.Context, u User) error { // fired on SignoutAllHandler. + // invalidate all previous tokens came from "u". + p.mu.Lock() + p.invalidatedAll[u.ID] = time.Now().Unix() + p.mu.Unlock() + + return nil +} diff --git a/_examples/auth/sso/views/layouts/main.html b/_examples/auth/sso/views/layouts/main.html new file mode 100644 index 00000000..c6efd56d --- /dev/null +++ b/_examples/auth/sso/views/layouts/main.html @@ -0,0 +1,30 @@ + + + + + + {{ if .Title }}{{ .Title }}{{ else }}Default Main Title{{ end }} + + + +
+
{{ template "content" . }}
+
{{ partial "partials/footer" .}}
+
+ + \ No newline at end of file diff --git a/_examples/auth/sso/views/partials/footer.html b/_examples/auth/sso/views/partials/footer.html new file mode 100644 index 00000000..69b8f9b8 --- /dev/null +++ b/_examples/auth/sso/views/partials/footer.html @@ -0,0 +1 @@ +Iris Web Framework © 2022 \ No newline at end of file diff --git a/_examples/auth/sso/views/signin.html b/_examples/auth/sso/views/signin.html new file mode 100644 index 00000000..57f7514c --- /dev/null +++ b/_examples/auth/sso/views/signin.html @@ -0,0 +1,9 @@ +
+
+ + + + + +
+
\ No newline at end of file diff --git a/_examples/file-server/webdav/main.go b/_examples/file-server/webdav/main.go new file mode 100644 index 00000000..0db07792 --- /dev/null +++ b/_examples/file-server/webdav/main.go @@ -0,0 +1,57 @@ +package main + +import ( + "net/http" + "os" + "strings" + + "github.com/kataras/iris/v12" + "github.com/kataras/iris/v12/middleware/accesslog" + "github.com/kataras/iris/v12/middleware/recover" + + "golang.org/x/net/webdav" +) + +func main() { + app := iris.New() + + app.Logger().SetLevel("debug") + app.Use(recover.New()) + app.Use(accesslog.New(os.Stdout).Handler) + + webdavHandler := &webdav.Handler{ + FileSystem: webdav.Dir("./"), + LockSystem: webdav.NewMemLS(), + Logger: func(r *http.Request, err error) { + if err != nil { + app.Logger().Error(err) + } + }, + } + + app.HandleMany(strings.Join(iris.WebDAVMethods, " "), "/{p:path}", iris.FromStd(webdavHandler)) + + app.Listen(":8080", + iris.WithoutServerError(iris.ErrServerClosed), + iris.WithoutPathCorrection, + ) +} + +/* Test with cURL or postman: + +* List files: + curl --location --request PROPFIND 'http://localhost:8080' +* Get File: + curl --location --request GET 'http://localhost:8080/test.txt' +* Upload File: + curl --location --request PUT 'http://localhost:8080/newfile.txt' \ + --header 'Content-Type: text/plain' \ + --data-raw 'This is a new file!' +* Copy File: + curl --location --request COPY 'http://localhost:8080/test.txt' \ + --header 'Destination: newdir/test.txt' +* Create New Directory: + curl --location --request MKCOL 'http://localhost:8080/anewdir/' + +And e.t.c. +*/ diff --git a/_examples/file-server/webdav/newdir/.gitkeep b/_examples/file-server/webdav/newdir/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/_examples/file-server/webdav/test.txt b/_examples/file-server/webdav/test.txt new file mode 100644 index 00000000..af5626b4 --- /dev/null +++ b/_examples/file-server/webdav/test.txt @@ -0,0 +1 @@ +Hello, world! diff --git a/_examples/mvc/grpc-compatible/main.go b/_examples/mvc/grpc-compatible/main.go index f93bf7a1..3ef16174 100644 --- a/_examples/mvc/grpc-compatible/main.go +++ b/_examples/mvc/grpc-compatible/main.go @@ -2,6 +2,7 @@ package main import ( "context" + "log" pb "github.com/kataras/iris/v12/_examples/mvc/grpc-compatible/helloworld" @@ -47,17 +48,32 @@ func newApp() *iris.Application { // Register MVC application controller for gRPC services. // You can bind as many mvc gRpc services in the same Party or app, // as the ServiceName differs. - mvc.New(app).Handle(ctrl, mvc.GRPC{ - Server: grpcServer, // Required. - ServiceName: "helloworld.Greeter", // Required. - Strict: false, - }) + mvc.New(app). + Register(new(myService)). + Handle(ctrl, mvc.GRPC{ + Server: grpcServer, // Required. + ServiceName: "helloworld.Greeter", // Required. + Strict: false, + }) return app } +type service interface { + DoSomething() error +} + +type myService struct{} + +func (s *myService) DoSomething() error { + log.Println("service: DoSomething") + return nil +} + type myController struct { // Ctx iris.Context + + SingletonDependency service } // SayHello implements helloworld.GreeterServer. @@ -70,5 +86,10 @@ type myController struct { // @Success 200 {string} string "Hello {name}" // @Router /helloworld.Greeter/SayHello [post] func (c *myController) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) { + err := c.SingletonDependency.DoSomething() + if err != nil { + return nil, err + } + return &pb.HelloReply{Message: "Hello " + in.GetName()}, nil } diff --git a/_examples/mvc/websocket-sso/browser/index.html b/_examples/mvc/websocket-sso/browser/index.html new file mode 100644 index 00000000..09bd4498 --- /dev/null +++ b/_examples/mvc/websocket-sso/browser/index.html @@ -0,0 +1,106 @@ + + + + Online visitors MVC example + + + + +
+ 1 online visitor +
+ + + + + + + + +

+    
+    
+    
+
+
+
+
\ No newline at end of file
diff --git a/_examples/mvc/websocket-sso/main.go b/_examples/mvc/websocket-sso/main.go
new file mode 100644
index 00000000..1e59e264
--- /dev/null
+++ b/_examples/mvc/websocket-sso/main.go
@@ -0,0 +1,72 @@
+//go:build go1.18
+
+package main
+
+import (
+	"fmt"
+
+	"github.com/kataras/iris/v12"
+	"github.com/kataras/iris/v12/mvc"
+	"github.com/kataras/iris/v12/sso"
+	"github.com/kataras/iris/v12/websocket"
+)
+
+// $ go run .
+func main() {
+	app := newApp()
+
+	// http://localhost:8080/signin (creds: kataras2006@hotmail.com 123456)
+	// http://localhost:8080/protected
+	// http://localhost:8080/signout
+	app.Listen(":8080")
+}
+
+func newApp() *iris.Application {
+	app := iris.New()
+
+	// Auth part.
+	app.RegisterView(iris.Blocks(iris.Dir("./views"), ".html").
+		LayoutDir("layouts").
+		Layout("main"))
+
+	s := sso.MustLoad[User]("./sso.yml")
+	s.AddProvider(NewProvider())
+
+	app.Get("/signin", renderSigninForm)
+	app.Post("/signin", s.SigninHandler)
+	app.Get("/signout", s.SignoutHandler)
+	//
+
+	websocketAPI := app.Party("/protected")
+	websocketAPI.Use(s.VerifyHandler())
+	websocketAPI.HandleDir("/", iris.Dir("./browser")) // render the ./browser/index.html.
+
+	websocketMVC := mvc.New(websocketAPI)
+	websocketMVC.HandleWebsocket(new(websocketController))
+	websocketServer := websocket.New(websocket.DefaultGorillaUpgrader, websocketMVC)
+	websocketAPI.Get("/ws", s.VerifyHandler() /* optional */, websocket.Handler(websocketServer))
+
+	return app
+}
+
+func renderSigninForm(ctx iris.Context) {
+	ctx.View("signin", iris.Map{"Title": "Signin Page"})
+}
+
+type websocketController struct {
+	*websocket.NSConn `stateless:"true"`
+}
+
+func (c *websocketController) Namespace() string {
+	return "default"
+}
+
+func (c *websocketController) OnChat(msg websocket.Message) error {
+	ctx := websocket.GetContext(c.Conn)
+	user := sso.GetUser[User](ctx)
+
+	msg.Body = []byte(fmt.Sprintf("%s: %s", user.Email, string(msg.Body)))
+	c.Conn.Server().Broadcast(c, msg)
+
+	return nil
+}
diff --git a/_examples/mvc/websocket-sso/sso.yml b/_examples/mvc/websocket-sso/sso.yml
new file mode 100644
index 00000000..c25a22bc
--- /dev/null
+++ b/_examples/mvc/websocket-sso/sso.yml
@@ -0,0 +1,32 @@
+Cookie: # optional.
+  Name: "iris_sso"
+  Hash: "D*G-KaPdSgUkXp2s5v8y/B?E(H+MbQeThWmYq3t6w9z$C&F)J@NcRfUjXn2r4u7x" # length of 64 characters (512-bit).
+  Block: "VkYp3s6v9y$B&E)H@McQfTjWmZq4t7w!" # length of 32 characters (256-bit).
+Keys:
+  - ID: IRIS_SSO_ACCESS # required.
+    Alg: EdDSA
+    MaxAge: 2h # 2 hours lifetime for access tokens. 
+    Private: |+
+      -----BEGIN PRIVATE KEY-----
+      MC4CAQAwBQYDK2VwBCIEIFdZWoDdFny5SMnP9Fyfr8bafi/B527EVZh8JJjDTIFO
+      -----END PRIVATE KEY-----
+    Public: |+
+      -----BEGIN PUBLIC KEY-----
+      MCowBQYDK2VwAyEAzpgjKSr9E032DX+foiOxq1QDsbzjLxagTN+yVpGWZB4=
+      -----END PUBLIC KEY-----
+  - ID: IRIS_SSO_REFRESH # optional. Good practise to have it though.
+    Alg: EdDSA
+    # 1 month lifetime for refresh tokens,
+    # after that period the user has to signin again.
+    MaxAge: 720h
+    Private: |+
+      -----BEGIN PRIVATE KEY-----
+      MC4CAQAwBQYDK2VwBCIEIHJ1aoIjA2sRp5eqGjGR3/UMucrHbBdBv9p8uwfzZ1KZ
+      -----END PRIVATE KEY-----
+    Public: |+
+      -----BEGIN PUBLIC KEY-----
+      MCowBQYDK2VwAyEAsKKAr+kDtfAqwG7cZdoEAfh9jHt9W8qi9ur5AA1KQAQ=
+      -----END PUBLIC KEY-----
+    # Example of setting a binary form of the encryption key for refresh tokens,
+    # it could be a "string" as well.
+    EncryptionKey: !!binary stSNLTu91YyihPxzeEOXKwGVMG00CjcC/68G8nMgmqA=
diff --git a/_examples/mvc/websocket-sso/user.go b/_examples/mvc/websocket-sso/user.go
new file mode 100644
index 00000000..a2e93017
--- /dev/null
+++ b/_examples/mvc/websocket-sso/user.go
@@ -0,0 +1,33 @@
+//go:build go1.18
+
+package main
+
+type AccessRole uint16
+
+func (r AccessRole) Is(v AccessRole) bool {
+	return r&v != 0
+}
+
+func (r AccessRole) Allow(v AccessRole) bool {
+	return r&v >= v
+}
+
+const (
+	InvalidAccessRole AccessRole = 1 << iota
+	Read
+	Write
+	Delete
+
+	Owner  = Read | Write | Delete
+	Member = Read | Write
+)
+
+type User struct {
+	ID    string     `json:"id"`
+	Email string     `json:"email"`
+	Role  AccessRole `json:"role"`
+}
+
+func (u User) GetID() string {
+	return u.ID
+}
diff --git a/_examples/mvc/websocket-sso/user_provider.go b/_examples/mvc/websocket-sso/user_provider.go
new file mode 100644
index 00000000..a1846c15
--- /dev/null
+++ b/_examples/mvc/websocket-sso/user_provider.go
@@ -0,0 +1,100 @@
+//go:build go1.18
+
+package main
+
+import (
+	"context"
+	"fmt"
+	"sync"
+	"time"
+
+	"github.com/kataras/iris/v12/sso"
+)
+
+type Provider struct {
+	dataset []User
+
+	invalidated    map[string]struct{} // key = token. Entry is blocked.
+	invalidatedAll map[string]int64    // key = user id, value = timestamp. Issued before is consider invalid.
+	mu             sync.RWMutex
+}
+
+func NewProvider() *Provider {
+	return &Provider{
+		dataset: []User{
+			{
+				ID:    "id-1",
+				Email: "kataras2006@hotmail.com",
+				Role:  Owner,
+			},
+			{
+				ID:    "id-2",
+				Email: "example@example.com",
+				Role:  Member,
+			},
+		},
+		invalidated:    make(map[string]struct{}),
+		invalidatedAll: make(map[string]int64),
+	}
+}
+
+func (p *Provider) Signin(ctx context.Context, username, password string) (User, error) { // fired on SigninHandler.
+	// your database...
+	for _, user := range p.dataset {
+		if user.Email == username {
+			return user, nil
+		}
+	}
+
+	return User{}, fmt.Errorf("user not found")
+}
+
+func (p *Provider) ValidateToken(ctx context.Context, standardClaims sso.StandardClaims, u User) error { // fired on VerifyHandler.
+	// your database and checks of blocked tokens...
+
+	// check for specific token ids.
+	p.mu.RLock()
+	_, tokenBlocked := p.invalidated[standardClaims.ID]
+	if !tokenBlocked {
+		// this will disallow refresh tokens with issuer as the blocked access token as well.
+		if standardClaims.Issuer != "" {
+			_, tokenBlocked = p.invalidated[standardClaims.Issuer]
+		}
+	}
+	p.mu.RUnlock()
+
+	if tokenBlocked {
+		return fmt.Errorf("token was invalidated")
+	}
+	//
+
+	// check all tokens issuet before the "InvalidateToken" method was fired for this user.
+	p.mu.RLock()
+	ts, oldUserBlocked := p.invalidatedAll[u.ID]
+	p.mu.RUnlock()
+
+	if oldUserBlocked && standardClaims.IssuedAt <= ts {
+		return fmt.Errorf("token was invalidated")
+	}
+	//
+
+	return nil // else valid.
+}
+
+func (p *Provider) InvalidateToken(ctx context.Context, standardClaims sso.StandardClaims, u User) error { // fired on SignoutHandler.
+	// invalidate this specific token.
+	p.mu.Lock()
+	p.invalidated[standardClaims.ID] = struct{}{}
+	p.mu.Unlock()
+
+	return nil
+}
+
+func (p *Provider) InvalidateTokens(ctx context.Context, u User) error { // fired on SignoutAllHandler.
+	// invalidate all previous tokens came from "u".
+	p.mu.Lock()
+	p.invalidatedAll[u.ID] = time.Now().Unix()
+	p.mu.Unlock()
+
+	return nil
+}
diff --git a/_examples/mvc/websocket-sso/views/layouts/main.html b/_examples/mvc/websocket-sso/views/layouts/main.html
new file mode 100644
index 00000000..c6efd56d
--- /dev/null
+++ b/_examples/mvc/websocket-sso/views/layouts/main.html
@@ -0,0 +1,30 @@
+
+
+
+    
+    
+    {{ if .Title }}{{ .Title }}{{ else }}Default Main Title{{ end }}
+
+
+
+    
+
{{ template "content" . }}
+
{{ partial "partials/footer" .}}
+
+ + \ No newline at end of file diff --git a/_examples/mvc/websocket-sso/views/partials/footer.html b/_examples/mvc/websocket-sso/views/partials/footer.html new file mode 100644 index 00000000..69b8f9b8 --- /dev/null +++ b/_examples/mvc/websocket-sso/views/partials/footer.html @@ -0,0 +1 @@ +Iris Web Framework © 2022 \ No newline at end of file diff --git a/_examples/mvc/websocket-sso/views/signin.html b/_examples/mvc/websocket-sso/views/signin.html new file mode 100644 index 00000000..57f7514c --- /dev/null +++ b/_examples/mvc/websocket-sso/views/signin.html @@ -0,0 +1,9 @@ +
+
+ + + + + +
+
\ No newline at end of file diff --git a/aliases.go b/aliases.go index da362586..6c18ebcb 100644 --- a/aliases.go +++ b/aliases.go @@ -660,8 +660,41 @@ const ( StatusNetworkReadTimeout = context.StatusNetworkReadTimeout ) -// StatusText returns a text for the HTTP status code. It returns the empty -// string if the code is unknown. -// -// Shortcut for core/router#StatusText. -var StatusText = context.StatusText +var ( + // StatusText returns a text for the HTTP status code. It returns the empty + // string if the code is unknown. + // + // Shortcut for core/router#StatusText. + StatusText = context.StatusText + // RegisterMethods adds custom http methods to the "AllMethods" list. + // Use it on initialization of your program. + // + // Shortcut for core/router#RegisterMethods. + RegisterMethods = router.RegisterMethods + + // WebDAVMethods contains a list of WebDAV HTTP Verbs. + // Register using RegiterMethods package-level function or + // through HandleMany party-level method. + WebDAVMethods = []string{ + MethodGet, + MethodHead, + MethodPatch, + MethodPut, + MethodPost, + MethodDelete, + MethodOptions, + MethodConnect, + MethodTrace, + "MKCOL", + "COPY", + "MOVE", + "LOCK", + "UNLOCK", + "PROPFIND", + "PROPPATCH", + "LINK", + "UNLINK", + "PURGE", + "VIEW", + } +) diff --git a/context/context.go b/context/context.go index ca479f36..e1944f4e 100644 --- a/context/context.go +++ b/context/context.go @@ -5314,7 +5314,11 @@ type SecureCookie interface { // // Example: https://github.com/kataras/iris/tree/master/_examples/cookies/securecookie func CookieEncoding(encoding SecureCookie, cookieNames ...string) CookieOption { - return func(_ *Context, c *http.Cookie, op uint8) { + if encoding == nil { + return func(_ *Context, _ *http.Cookie, _ uint8) {} + } + + return func(ctx *Context, c *http.Cookie, op uint8) { if op == OpCookieDel { return } @@ -5328,10 +5332,12 @@ func CookieEncoding(encoding SecureCookie, cookieNames ...string) CookieOption { // Should encode, it's a write to the client operation. newVal, err := encoding.Encode(c.Name, c.Value) if err != nil { + ctx.Application().Logger().Error(err) c.Value = "" } else { c.Value = newVal } + return case OpCookieGet: // Should decode, it's a read from the client operation. diff --git a/core/router/api_builder.go b/core/router/api_builder.go index aa220470..f0a14874 100644 --- a/core/router/api_builder.go +++ b/core/router/api_builder.go @@ -39,6 +39,13 @@ var AllMethods = []string{ http.MethodTrace, } +// RegisterMethods adds custom http methods to the "AllMethods" list. +// Use it on initialization of your program. +func RegisterMethods(newCustomHTTPVerbs ...string) { + newMethods := append(AllMethods, newCustomHTTPVerbs...) + AllMethods = removeDuplicates(newMethods) +} + // repository passed to all parties(subrouters), it's the object witch keeps // all the routes. type repository struct { diff --git a/go.mod b/go.mod index a1d99da5..30959e4c 100644 --- a/go.mod +++ b/go.mod @@ -18,13 +18,14 @@ require ( github.com/goccy/go-json v0.9.5 github.com/golang/snappy v0.0.4 github.com/google/uuid v1.3.0 + github.com/gorilla/securecookie v1.1.1 github.com/iris-contrib/httpexpect/v2 v2.3.1 github.com/iris-contrib/jade v1.1.4 github.com/iris-contrib/schema v0.0.6 github.com/json-iterator/go v1.1.12 github.com/kataras/blocks v0.0.5 github.com/kataras/golog v0.1.7 - github.com/kataras/jwt v0.1.5 + github.com/kataras/jwt v0.1.8 github.com/kataras/neffos v0.0.19 github.com/kataras/pio v0.0.10 github.com/kataras/sitemap v0.0.5 diff --git a/go.sum b/go.sum index 379cb4aa..d117201f 100644 --- a/go.sum +++ b/go.sum @@ -85,6 +85,8 @@ github.com/gopherjs/gopherjs v0.0.0-20220221023154-0b2280d3ff96 h1:QJq7UBOuoynsy github.com/gopherjs/gopherjs v0.0.0-20220221023154-0b2280d3ff96/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k= github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= +github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= +github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= @@ -109,8 +111,8 @@ github.com/kataras/blocks v0.0.5 h1:jFrsHEDfXZhHTbhkNWgMgpfEQNj1Bwr1IYEYZ9Xxoxg= github.com/kataras/blocks v0.0.5/go.mod h1:kcJIuvuA8QmGKFLHIZHdCAPCjcE85IhttzXd6W+ayfE= github.com/kataras/golog v0.1.7 h1:0TY5tHn5L5DlRIikepcaRR/6oInIr9AiWsxzt0vvlBE= github.com/kataras/golog v0.1.7/go.mod h1:jOSQ+C5fUqsNSwurB/oAHq1IFSb0KI3l6GMa7xB6dZA= -github.com/kataras/jwt v0.1.5 h1:3UScbsLyo7fsKP6IRPzySH0mcAdTsEu104iWMjGqEyE= -github.com/kataras/jwt v0.1.5/go.mod h1:4ss3aGJi58q3YGmhLUiOvNJnL7UlTXD7+Wf+skgsTmQ= +github.com/kataras/jwt v0.1.8 h1:u71baOsYD22HWeSOg32tCHbczPjdCk7V4MMeJqTtmGk= +github.com/kataras/jwt v0.1.8/go.mod h1:Q5j2IkcIHnfwy+oNY3TVWuEBJNw0ADgCcXK9CaZwV4o= github.com/kataras/neffos v0.0.19 h1:j3jp/hzvGFQjnkkLWGNjae5qMSdpMYr66Lxgf8CgcAw= github.com/kataras/neffos v0.0.19/go.mod h1:CAAuFqHYX5t0//LLMiVWooOSp5FPeBRD8cn/892P1JE= github.com/kataras/pio v0.0.10 h1:b0qtPUqOpM2O+bqa5wr2O6dN4cQNwSmFd6HQqgVae0g= diff --git a/sso/configuration.go b/sso/configuration.go new file mode 100644 index 00000000..e3100788 --- /dev/null +++ b/sso/configuration.go @@ -0,0 +1,162 @@ +//go:build go1.18 + +package sso + +import ( + "encoding/json" + "errors" + "fmt" + "os" + "path/filepath" + "time" + + "github.com/gorilla/securecookie" + "github.com/kataras/jwt" + "gopkg.in/yaml.v3" +) + +const ( + KIDAccess = "IRIS_SSO_ACCESS" + KIDRefresh = "IRIS_SSO_REFRESH" +) + +type ( + Configuration struct { + Cookie CookieConfiguration `json:"cookie" yaml:"Cookie" toml:"Cookie" ini:"cookie"` + // keep it to always renew the refresh token. RefreshStrategy string `json:"refresh_strategy" yaml:"RefreshStrategy" toml:"RefreshStrategy" ini:"refresh_strategy"` + Keys jwt.KeysConfiguration `json:"keys" yaml:"Keys" toml:"Keys" ini:"keys"` + } + + CookieConfiguration struct { + Name string `json:"cookie" yaml:"Name" toml:"Name" ini:"name"` + Hash string `json:"hash" yaml:"Hash" toml:"Hash" ini:"hash"` + Block string `json:"block" yaml:"Block" toml:"Block" ini:"block"` + } +) + +func (c *Configuration) validate() (jwt.Keys, error) { + if c.Cookie.Name != "" { + if c.Cookie.Hash == "" || c.Cookie.Block == "" { + return nil, fmt.Errorf("cookie block and cookie hash are required for security reasons when cookie is used") + } + } + + keys, err := c.Keys.Load() + if err != nil { + return nil, fmt.Errorf("sso: %w", err) + } + + if _, ok := keys[KIDAccess]; !ok { + return nil, fmt.Errorf("sso: %s access token is missing from the configuration", KIDAccess) + } + + // Let's keep refresh optional. + // if _, ok := keys[KIDRefresh]; !ok { + // return nil, fmt.Errorf("sso: %s refresh token is missing from the configuration", KIDRefresh) + // } + return keys, nil +} + +// BindRandom binds the "c" configuration to random values for keys and cookie security. +// Keys will not be persisted between restarts, +// a more persistent storage should be considered for production applications. +func (c *Configuration) BindRandom() error { + accessPublic, accessPrivate, err := jwt.GenerateEdDSA() + if err != nil { + return err + } + + refreshPublic, refreshPrivate, err := jwt.GenerateEdDSA() + if err != nil { + return err + } + + *c = Configuration{ + Cookie: CookieConfiguration{ + Name: "iris_sso", + Hash: string(securecookie.GenerateRandomKey(64)), + Block: string(securecookie.GenerateRandomKey(32)), + }, + Keys: jwt.KeysConfiguration{ + { + ID: KIDAccess, + Alg: jwt.EdDSA.Name(), + MaxAge: 2 * time.Hour, + Public: string(accessPublic), + Private: string(accessPrivate), + }, + { + ID: KIDRefresh, + Alg: jwt.EdDSA.Name(), + MaxAge: 720 * time.Hour, + Public: string(refreshPublic), + Private: string(refreshPrivate), + EncryptionKey: string(jwt.MustGenerateRandom(32)), + }, + }, + } + + return nil +} + +func (c *Configuration) BindFile(filename string) error { + switch filepath.Ext(filename) { + case ".json": + contents, err := os.ReadFile(filename) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + generatedConfig := MustGenerateConfiguration() + if generatedYAML, gErr := generatedConfig.ToJSON(); gErr == nil { + err = fmt.Errorf("%w: example:\n\n%s", err, generatedYAML) + } + } + return err + } + + return json.Unmarshal(contents, c) + default: + contents, err := os.ReadFile(filename) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + generatedConfig := MustGenerateConfiguration() + if generatedYAML, gErr := generatedConfig.ToYAML(); gErr == nil { + err = fmt.Errorf("%w: example:\n\n%s", err, generatedYAML) + } + } + return err + } + + return yaml.Unmarshal(contents, c) + } + +} + +func (c *Configuration) ToYAML() ([]byte, error) { + return yaml.Marshal(c) +} + +func (c *Configuration) ToJSON() ([]byte, error) { + return json.Marshal(c) +} + +func MustGenerateConfiguration() (c Configuration) { + if err := c.BindRandom(); err != nil { + panic(err) + } + + return +} + +func LoadConfiguration(filename string) (c Configuration, err error) { + err = c.BindFile(filename) + return +} + +func MustLoadConfiguration(filename string) Configuration { + c, err := LoadConfiguration(filename) + if err != nil { + panic(err) + } + + return c +} diff --git a/sso/provider.go b/sso/provider.go new file mode 100644 index 00000000..816c2d79 --- /dev/null +++ b/sso/provider.go @@ -0,0 +1,83 @@ +//go:build go1.18 + +package sso + +import ( + stdContext "context" + "fmt" + + "github.com/kataras/iris/v12/context" + "github.com/kataras/iris/v12/middleware/jwt" + "github.com/kataras/iris/v12/x/errors" +) + +type VerifiedToken = jwt.VerifiedToken + +type Provider[T User] interface { // A provider can implement Transformer and ErrorHandler as well. + Signin(ctx stdContext.Context, username, password string) (T, error) + + // We could do this instead of transformer below but let's keep separated logic methods: + // ValidateToken(ctx context.Context, tok *VerifiedToken, t *T) error + ValidateToken(ctx stdContext.Context, standardClaims StandardClaims, t T) error + + InvalidateToken(ctx stdContext.Context, standardClaims StandardClaims, t T) error + InvalidateTokens(ctx stdContext.Context, t T) error +} + +// ClaimsProvider is an optional interface, which may not be used at all. +// If completed by a Provider, it signs the jwt token +// using these claims to each of the following token types. +type ClaimsProvider interface { + GetAccessTokenClaims() StandardClaims + GetRefreshTokenClaims(accessClaims StandardClaims) StandardClaims +} + +type Transformer[T User] interface { + Transform(ctx stdContext.Context, tok *VerifiedToken) (T, error) +} + +type TransformerFunc[T User] func(ctx stdContext.Context, tok *VerifiedToken) (T, error) + +func (fn TransformerFunc[T]) Transform(ctx stdContext.Context, tok *VerifiedToken) (T, error) { + return fn(ctx, tok) +} + +type ErrorHandler interface { + InvalidArgument(ctx *context.Context, err error) + Unauthenticated(ctx *context.Context, err error) +} + +type DefaultErrorHandler struct{} + +func (e *DefaultErrorHandler) InvalidArgument(ctx *context.Context, err error) { + errors.InvalidArgument.Details(ctx, "unable to parse body", err.Error()) +} + +func (e *DefaultErrorHandler) Unauthenticated(ctx *context.Context, err error) { + errors.Unauthenticated.Err(ctx, err) +} + +type provider[T User] struct{} + +func newProvider[T User]() *provider[T] { + return new(provider[T]) +} + +func (p *provider[T]) Signin(ctx stdContext.Context, username, password string) (T, error) { // fired on SigninHandler. + // your database... + var t T + return t, fmt.Errorf("user not found") +} + +func (p *provider[T]) ValidateToken(ctx stdContext.Context, standardClaims StandardClaims, t T) error { // fired on VerifyHandler. + // your database and checks of blocked tokens... + return nil +} + +func (p *provider[T]) InvalidateToken(ctx stdContext.Context, standardClaims StandardClaims, t T) error { // fired on SignoutHandler. + return nil +} + +func (p *provider[T]) InvalidateTokens(ctx stdContext.Context, t T) error { // fired on SignoutAllHandler. + return nil +} diff --git a/sso/sso.go b/sso/sso.go new file mode 100644 index 00000000..047b4756 --- /dev/null +++ b/sso/sso.go @@ -0,0 +1,568 @@ +//go:build go1.18 + +package sso + +import ( + stdContext "context" + "fmt" + "net/http" + "net/url" + "strings" + "time" + + "github.com/kataras/iris/v12/context" + + "github.com/google/uuid" + "github.com/gorilla/securecookie" + "github.com/kataras/jwt" +) + +type ( + SSO[T User] struct { + config Configuration + + keys jwt.Keys + securecookie context.SecureCookie + + providers []Provider[T] // at least one. + errorHandler ErrorHandler + transformer Transformer[T] + claimsProvider ClaimsProvider + refreshEnabled bool // if KIDRefresh exists in keys. + } + + TVerify[T User] func(t T) error + + SigninRequest struct { + Username string `json:"username" form:"username,omitempty"` // username OR email, username has priority over email. + Email string `json:"email" form:"email,omitempty"` // email OR username. + Password string `json:"password" form:"password"` + } + + SigninResponse struct { + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token,omitempty"` + } + + RefreshRequest struct { + RefreshToken string `json:"refresh_token"` + } +) + +func MustLoad[T User](filename string) *SSO[T] { + var config Configuration + if err := config.BindFile(filename); err != nil { + panic(err) + } + + s, err := New[T](config) + if err != nil { + panic(err) + } + + return s +} + +func Must[T User](s *SSO[T], err error) *SSO[T] { + if err != nil { + panic(err) + } + + return s +} + +func New[T User](config Configuration) (*SSO[T], error) { + keys, err := config.validate() + if err != nil { + return nil, err + } + _, refreshEnabled := keys[KIDRefresh] + + s := &SSO[T]{ + config: config, + keys: keys, + securecookie: securecookie.New([]byte(config.Cookie.Hash), []byte(config.Cookie.Block)), + refreshEnabled: refreshEnabled, + // providers: []Provider[T]{newProvider[T]()}, + errorHandler: new(DefaultErrorHandler), + } + + return s, nil +} + +func (s *SSO[T]) WithProviderAndErrorHandler(provider Provider[T], errHandler ErrorHandler) *SSO[T] { + if provider != nil { + for i := range s.providers { + s.providers[i] = nil + } + s.providers = nil + + s.providers = make([]Provider[T], 0, 1) + s.AddProvider(provider) + } + + if errHandler != nil { + s.SetErrorHandler(errHandler) + } + + return s +} + +func (s *SSO[T]) AddProvider(providers ...Provider[T]) *SSO[T] { + // defaultProviderTypename := strings.Replace(fmt.Sprintf("%T", s), "SSO", "provider", 1) + // if len(s.providers) == 1 && fmt.Sprintf("%T", s.providers[0]) == defaultProviderTypename { + // s.providers = append(s.providers[1:], p...) + + // A provider can also implement both transformer and + // error handler if that's the design option of the end-developer. + for _, p := range providers { + if s.transformer == nil { + if transformer, ok := p.(Transformer[T]); ok { + s.SetTransformer(transformer) + } + } + + if errHandler, ok := p.(ErrorHandler); ok { + s.SetErrorHandler(errHandler) + } + + if s.claimsProvider == nil { + if claimsProvider, ok := p.(ClaimsProvider); ok { + s.claimsProvider = claimsProvider + } + } + } + + s.providers = append(s.providers, providers...) + return s +} + +func (s *SSO[T]) SetErrorHandler(errHandler ErrorHandler) *SSO[T] { + s.errorHandler = errHandler + return s +} + +func (s *SSO[T]) SetTransformer(transformer Transformer[T]) *SSO[T] { + s.transformer = transformer + return s +} + +func (s *SSO[T]) SetTransformerFunc(transfermerFunc func(ctx stdContext.Context, tok *VerifiedToken) (T, error)) *SSO[T] { + s.transformer = TransformerFunc[T](transfermerFunc) + return s +} + +func (s *SSO[T]) Signin(ctx stdContext.Context, username, password string) ([]byte, []byte, error) { + var t T + + // get "t" from a valid provider. + if n := len(s.providers); n > 0 { + for i := 0; i < n; i++ { + p := s.providers[i] + + v, err := p.Signin(ctx, username, password) + if err != nil { + if i == n-1 { // last provider errored. + return nil, nil, fmt.Errorf("sso: signin: %w", err) + } + // keep searching. + continue + } + + // found. + t = v + break + } + } else { + return nil, nil, fmt.Errorf("sso: signin: no provider") + } + + // sign the tokens. + accessToken, refreshToken, err := s.sign(t) + if err != nil { + return nil, nil, fmt.Errorf("sso: signin: %w", err) + } + + return accessToken, refreshToken, nil +} + +func (s *SSO[T]) sign(t T) ([]byte, []byte, error) { + // sign the tokens. + var ( + accessStdClaims StandardClaims + refreshStdClaims StandardClaims + ) + + if s.claimsProvider != nil { + accessStdClaims = s.claimsProvider.GetAccessTokenClaims() + refreshStdClaims = s.claimsProvider.GetRefreshTokenClaims(accessStdClaims) + } + + iat := jwt.Clock().Unix() + + if accessStdClaims.IssuedAt == 0 { + accessStdClaims.IssuedAt = iat + } + + if accessStdClaims.ID == "" { + accessStdClaims.ID = uuid.NewString() + } + + if refreshStdClaims.IssuedAt == 0 { + refreshStdClaims.IssuedAt = iat + } + + if refreshStdClaims.ID == "" { + refreshStdClaims.ID = uuid.NewString() + } + + if refreshStdClaims.OriginID == "" { + // keep a reference of the access token the refresh token is created, + // if that access token is invalidated then + // its refresh token should be too so the user can force-login. + refreshStdClaims.OriginID = accessStdClaims.ID + } + + accessToken, err := s.keys.SignToken(KIDAccess, t, accessStdClaims) + if err != nil { + return nil, nil, fmt.Errorf("access: %w", err) + } + + var refreshToken []byte + if s.refreshEnabled { + refreshToken, err = s.keys.SignToken(KIDRefresh, t, refreshStdClaims) + if err != nil { + return nil, nil, fmt.Errorf("refresh: %w", err) + } + } + + return accessToken, refreshToken, nil +} + +func (s *SSO[T]) SigninHandler(ctx *context.Context) { + // No, let the developer decide it based on a middleware, e.g. iris.LimitRequestBodySize. + // ctx.SetMaxRequestBodySize(s.maxRequestBodySize) + + var ( + req SigninRequest + err error + ) + + switch ctx.GetContentTypeRequested() { + case context.ContentFormHeaderValue, context.ContentFormMultipartHeaderValue: + err = ctx.ReadForm(&req) + default: + err = ctx.ReadJSON(&req) + } + + if err != nil { + s.errorHandler.InvalidArgument(ctx, err) + return + } + + if req.Username == "" { + req.Username = req.Email + } + + accessTokenBytes, refreshTokenBytes, err := s.Signin(ctx, req.Username, req.Password) + if err != nil { + s.tryRemoveCookie(ctx) // remove cookie on invalidated. + + s.errorHandler.Unauthenticated(ctx, err) + return + } + accessToken := jwt.BytesToString(accessTokenBytes) + refreshToken := jwt.BytesToString(refreshTokenBytes) + + s.trySetCookie(ctx, accessToken) + + resp := SigninResponse{ + AccessToken: accessToken, + RefreshToken: refreshToken, + } + ctx.JSON(resp) +} + +func (s *SSO[T]) Verify(ctx stdContext.Context, token []byte) (T, StandardClaims, error) { + t, claims, err := s.verify(ctx, token) + if err != nil { + return t, StandardClaims{}, fmt.Errorf("sso: verify: %w", err) + } + + return t, claims, nil +} + +func (s *SSO[T]) verify(ctx stdContext.Context, token []byte) (T, StandardClaims, error) { + var t T + + if len(token) == 0 { // should never happen at this state. + return t, StandardClaims{}, jwt.ErrMissing + } + + verifiedToken, err := jwt.VerifyWithHeaderValidator(nil, nil, token, s.keys.ValidateHeader, jwt.Leeway(time.Minute)) + if err != nil { + return t, StandardClaims{}, err + } + + if s.transformer != nil { + if t, err = s.transformer.Transform(ctx, verifiedToken); err != nil { + return t, StandardClaims{}, err + } + } else { + if err = verifiedToken.Claims(&t); err != nil { + return t, StandardClaims{}, err + } + } + + standardClaims := verifiedToken.StandardClaims + + if n := len(s.providers); n > 0 { + for i := 0; i < n; i++ { + p := s.providers[i] + + err := p.ValidateToken(ctx, standardClaims, t) + if err != nil { + if i == n-1 { // last provider errored. + return t, StandardClaims{}, err + } + // keep searching. + continue + } + + // token is allowed. + break + } + } else { + // return t, StandardClaims{}, fmt.Errorf("no provider") + } + + return t, standardClaims, nil +} + +/* Good idea but not practical. +func Transform[T User, V User](transformer Transformer[T, V]) context.Handler { + return func(ctx *context.Context) { + existingUserValue := GetUser[T](ctx) + newUserValue, err := transformer.Transform(ctx, existingUserValue) + if err != nil { + ctx.SetErr(err) + return + } + + ctx.Values().Set(userContextKey, newUserValue) + ctx.Next() + } +} +*/ + +func (s *SSO[T]) VerifyHandler(verifyFuncs ...TVerify[T]) context.Handler { + return func(ctx *context.Context) { + accessToken := s.extractAccessToken(ctx) + + if accessToken == "" { // if empty, fire 401. + s.errorHandler.Unauthenticated(ctx, jwt.ErrMissing) + return + } + + t, claims, err := s.Verify(ctx, []byte(accessToken)) + if err != nil { + s.errorHandler.Unauthenticated(ctx, err) + return + } + + for _, verify := range verifyFuncs { + if verify == nil { + continue + } + + if err = verify(t); err != nil { + err = fmt.Errorf("sso: verify: %v", err) + s.errorHandler.Unauthenticated(ctx, err) + return + } + } + + ctx.SetUser(t) + + // store the user to the request. + ctx.Values().Set(accessTokenContextKey, accessToken) + + ctx.Values().Set(userContextKey, t) + ctx.Values().Set(standardClaimsContextKey, claims) + + ctx.Next() + } +} + +func (s *SSO[T]) extractAccessToken(ctx *context.Context) string { + // first try from authorization: bearer header. + accessToken := extractTokenFromHeader(ctx) + + // then if no header, try try extract from cookie. + if accessToken == "" { + if cookieName := s.config.Cookie.Name; cookieName != "" { + accessToken = ctx.GetCookie(cookieName, context.CookieEncoding(s.securecookie)) + } + } + + return accessToken +} + +func (s *SSO[T]) Refresh(ctx stdContext.Context, refreshToken []byte) ([]byte, []byte, error) { + if !s.refreshEnabled { + return nil, nil, fmt.Errorf("sso: refresh: disabled") + } + + t, _, err := s.verify(ctx, refreshToken) + if err != nil { + return nil, nil, fmt.Errorf("sso: refresh: %w", err) + } + + // refresh the tokens, both refresh & access tokens will be renew to prevent + // malicious 😈 users that may hold a refresh token. + accessTok, refreshTok, err := s.sign(t) + if err != nil { + return nil, nil, fmt.Errorf("sso: refresh: %w", err) + } + + return accessTok, refreshTok, nil +} + +func (s *SSO[T]) RefreshHandler(ctx *context.Context) { + var req RefreshRequest + err := ctx.ReadJSON(&req) + if err != nil { + s.errorHandler.InvalidArgument(ctx, err) + return + } + + accessTokenBytes, refreshTokenBytes, err := s.Refresh(ctx, []byte(req.RefreshToken)) + if err != nil { + // s.tryRemoveCookie(ctx) + s.errorHandler.Unauthenticated(ctx, err) + return + } + + accessToken := jwt.BytesToString(accessTokenBytes) + refreshToken := jwt.BytesToString(refreshTokenBytes) + + s.trySetCookie(ctx, accessToken) + + resp := SigninResponse{ + AccessToken: accessToken, + RefreshToken: refreshToken, + } + ctx.JSON(resp) +} + +func (s *SSO[T]) Signout(ctx stdContext.Context, token []byte, all bool) error { + t, standardClaims, err := s.verify(ctx, token) + if err != nil { + return fmt.Errorf("sso: signout: verify: %w", err) + } + + for i, n := 0, len(s.providers)-1; i <= n; i++ { + p := s.providers[i] + + if all { + err = p.InvalidateTokens(ctx, t) + } else { + err = p.InvalidateToken(ctx, standardClaims, t) + } + + if err != nil { + if i == n { // last provider errored. + return err + } + // keep trying. + continue + } + + // token is marked as invalidated by a provider. + break + } + + return nil +} + +func (s *SSO[T]) SignoutHandler(ctx *context.Context) { + s.signoutHandler(ctx, false) +} + +func (s *SSO[T]) SignoutAllHandler(ctx *context.Context) { + s.signoutHandler(ctx, true) +} + +func (s *SSO[T]) signoutHandler(ctx *context.Context, all bool) { + accessToken := s.extractAccessToken(ctx) + if accessToken == "" { + s.errorHandler.Unauthenticated(ctx, jwt.ErrMissing) + return + } + + err := s.Signout(ctx, []byte(accessToken), all) + if err != nil { + s.errorHandler.Unauthenticated(ctx, err) + return + } + + s.tryRemoveCookie(ctx) + + ctx.SetUser(nil) + + ctx.Values().Remove(accessTokenContextKey) + ctx.Values().Remove(userContextKey) + ctx.Values().Remove(standardClaimsContextKey) +} + +var headerKeys = [...]string{ + "Authorization", + "X-Authorization", +} + +func extractTokenFromHeader(ctx *context.Context) string { + for _, headerKey := range headerKeys { + headerValue := ctx.GetHeader(headerKey) + if headerValue == "" { + continue + } + + // pure check: authorization header format must be Bearer {token} + authHeaderParts := strings.Split(headerValue, " ") + if len(authHeaderParts) != 2 || strings.ToLower(authHeaderParts[0]) != "bearer" { + continue + } + + return authHeaderParts[1] + } + + return "" +} + +func (s *SSO[T]) trySetCookie(ctx *context.Context, accessToken string) { + if cookieName := s.config.Cookie.Name; cookieName != "" { + maxAge := s.keys[KIDAccess].MaxAge + if maxAge == 0 { + maxAge = context.SetCookieKVExpiration + } + + cookie := &http.Cookie{ + Path: "/", + Name: cookieName, + Value: url.QueryEscape(accessToken), + HttpOnly: true, + Domain: ctx.Domain(), + SameSite: http.SameSiteLaxMode, + Expires: time.Now().Add(maxAge), + MaxAge: int(maxAge.Seconds()), + } + + ctx.SetCookie(cookie, context.CookieEncoding(s.securecookie)) + } +} + +func (s *SSO[T]) tryRemoveCookie(ctx *context.Context) { + if cookieName := s.config.Cookie.Name; cookieName != "" { + ctx.RemoveCookie(cookieName) + } +} diff --git a/sso/user.go b/sso/user.go new file mode 100644 index 00000000..5cd0b010 --- /dev/null +++ b/sso/user.go @@ -0,0 +1,53 @@ +//go:build go1.18 + +package sso + +import ( + "github.com/kataras/iris/v12/context" + + "github.com/kataras/jwt" +) + +type ( + StandardClaims = jwt.Claims + User = interface{} // any type. +) + +const accessTokenContextKey = "iris.sso.context.access_token" + +func GetAccessToken(ctx *context.Context) string { + return ctx.Values().GetString(accessTokenContextKey) +} + +const standardClaimsContextKey = "iris.sso.context.standard_claims" + +func GetStandardClaims(ctx *context.Context) StandardClaims { + if v := ctx.Values().Get(standardClaimsContextKey); v != nil { + if c, ok := v.(StandardClaims); ok { + return c + } + } + + return StandardClaims{} +} + +func (s *SSO[T]) GetStandardClaims(ctx *context.Context) StandardClaims { + return GetStandardClaims(ctx) +} + +const userContextKey = "iris.sso.context.user" + +func GetUser[T User](ctx *context.Context) T { + if v := ctx.Values().Get(userContextKey); v != nil { + if t, ok := v.(T); ok { + return t + } + } + + var empty T + return empty +} + +func (s *SSO[T]) GetUser(ctx *context.Context) T { + return GetUser[T](ctx) +} diff --git a/x/client/client_test.go b/x/client/client_test.go index 6d4563d4..c835b664 100644 --- a/x/client/client_test.go +++ b/x/client/client_test.go @@ -1,15 +1,15 @@ package client import ( - stdContext "context" + "context" + "encoding/json" + "net/http" + "net/http/httptest" "reflect" "testing" - - "github.com/kataras/iris/v12" - "github.com/kataras/iris/v12/httptest" ) -var defaultCtx = stdContext.Background() +var defaultCtx = context.Background() type testValue struct { Firstname string `json:"firstname"` @@ -18,40 +18,41 @@ type testValue struct { func TestClientJSON(t *testing.T) { expectedJSON := testValue{Firstname: "Makis"} - app := iris.New() - app.Get("/", sendJSON(t, expectedJSON)) + app := http.NewServeMux() + app.HandleFunc("/send", sendJSON(t, expectedJSON)) var irisGotJSON testValue - app.Post("/", readJSON(t, &irisGotJSON, &expectedJSON)) + app.HandleFunc("/read", readJSON(t, &irisGotJSON, &expectedJSON)) - srv := httptest.NewServer(t, app) + srv := httptest.NewServer(app) client := New(BaseURL(srv.URL)) // Test ReadJSON (read from server). var got testValue - if err := client.ReadJSON(defaultCtx, &got, iris.MethodGet, "/", nil); err != nil { + if err := client.ReadJSON(defaultCtx, &got, http.MethodGet, "/send", nil); err != nil { t.Fatal(err) } // Test JSON (send to server). - resp, err := client.JSON(defaultCtx, iris.MethodPost, "/", expectedJSON) + resp, err := client.JSON(defaultCtx, http.MethodPost, "/read", expectedJSON) if err != nil { t.Fatal(err) } client.DrainResponseBody(resp) } -func sendJSON(t *testing.T, v interface{}) iris.Handler { - return func(ctx iris.Context) { - if _, err := ctx.JSON(v); err != nil { +func sendJSON(t *testing.T, v interface{}) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + if err := json.NewEncoder(w).Encode(v); err != nil { t.Fatal(err) } } } -func readJSON(t *testing.T, ptr interface{}, expected interface{}) iris.Handler { - return func(ctx iris.Context) { - if err := ctx.ReadJSON(ptr); err != nil { +func readJSON(t *testing.T, ptr interface{}, expected interface{}) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if err := json.NewDecoder(r.Body).Decode(ptr); err != nil { t.Fatal(err) } diff --git a/x/errors/errors.go b/x/errors/errors.go index 5534d3b3..be9ee8c3 100644 --- a/x/errors/errors.go +++ b/x/errors/errors.go @@ -3,8 +3,9 @@ package errors import ( "encoding/json" "fmt" + "net/http" - "github.com/kataras/iris/v12" + "github.com/kataras/iris/v12/context" "github.com/kataras/iris/v12/x/client" ) @@ -13,11 +14,11 @@ import ( // // See "OnErrorLog" variable to change the way an error is logged, // by default the error is logged using the Application's Logger's Error method. -type LogErrorFunc = func(ctx iris.Context, err error) +type LogErrorFunc = func(ctx *context.Context, err error) // LogError can be modified to customize the way an error is logged to the server (most common: internal server errors, database errors et.c.). // Can be used to customize the error logging, e.g. using Sentry (cloud-based error console). -var LogError LogErrorFunc = func(ctx iris.Context, err error) { +var LogError LogErrorFunc = func(ctx *context.Context, err error) { ctx.Application().Logger().Error(err) } @@ -56,7 +57,7 @@ var errorCodeMap = make(map[ErrorCodeName]ErrorCode) // // Example: // var ( -// NotFound = errors.E("NOT_FOUND", iris.StatusNotFound) +// NotFound = errors.E("NOT_FOUND", http.StatusNotFound) // ) // ... // NotFound.Details(ctx, "resource not found", "user with id: %q was not found", userID) @@ -96,57 +97,57 @@ func RegisterErrorCodeMap(errorMap map[ErrorCodeName]int) { // List of default error codes a server should follow and send back to the client. var ( - Cancelled ErrorCodeName = E("CANCELLED", iris.StatusTokenRequired) - Unknown ErrorCodeName = E("UNKNOWN", iris.StatusInternalServerError) - InvalidArgument ErrorCodeName = E("INVALID_ARGUMENT", iris.StatusBadRequest) - DeadlineExceeded ErrorCodeName = E("DEADLINE_EXCEEDED", iris.StatusGatewayTimeout) - NotFound ErrorCodeName = E("NOT_FOUND", iris.StatusNotFound) - AlreadyExists ErrorCodeName = E("ALREADY_EXISTS", iris.StatusConflict) - PermissionDenied ErrorCodeName = E("PERMISSION_DENIED", iris.StatusForbidden) - Unauthenticated ErrorCodeName = E("UNAUTHENTICATED", iris.StatusUnauthorized) - ResourceExhausted ErrorCodeName = E("RESOURCE_EXHAUSTED", iris.StatusTooManyRequests) - FailedPrecondition ErrorCodeName = E("FAILED_PRECONDITION", iris.StatusBadRequest) - Aborted ErrorCodeName = E("ABORTED", iris.StatusConflict) - OutOfRange ErrorCodeName = E("OUT_OF_RANGE", iris.StatusBadRequest) - Unimplemented ErrorCodeName = E("UNIMPLEMENTED", iris.StatusNotImplemented) - Internal ErrorCodeName = E("INTERNAL", iris.StatusInternalServerError) - Unavailable ErrorCodeName = E("UNAVAILABLE", iris.StatusServiceUnavailable) - DataLoss ErrorCodeName = E("DATA_LOSS", iris.StatusInternalServerError) + Cancelled ErrorCodeName = E("CANCELLED", context.StatusTokenRequired) + Unknown ErrorCodeName = E("UNKNOWN", http.StatusInternalServerError) + InvalidArgument ErrorCodeName = E("INVALID_ARGUMENT", http.StatusBadRequest) + DeadlineExceeded ErrorCodeName = E("DEADLINE_EXCEEDED", http.StatusGatewayTimeout) + NotFound ErrorCodeName = E("NOT_FOUND", http.StatusNotFound) + AlreadyExists ErrorCodeName = E("ALREADY_EXISTS", http.StatusConflict) + PermissionDenied ErrorCodeName = E("PERMISSION_DENIED", http.StatusForbidden) + Unauthenticated ErrorCodeName = E("UNAUTHENTICATED", http.StatusUnauthorized) + ResourceExhausted ErrorCodeName = E("RESOURCE_EXHAUSTED", http.StatusTooManyRequests) + FailedPrecondition ErrorCodeName = E("FAILED_PRECONDITION", http.StatusBadRequest) + Aborted ErrorCodeName = E("ABORTED", http.StatusConflict) + OutOfRange ErrorCodeName = E("OUT_OF_RANGE", http.StatusBadRequest) + Unimplemented ErrorCodeName = E("UNIMPLEMENTED", http.StatusNotImplemented) + Internal ErrorCodeName = E("INTERNAL", http.StatusInternalServerError) + Unavailable ErrorCodeName = E("UNAVAILABLE", http.StatusServiceUnavailable) + DataLoss ErrorCodeName = E("DATA_LOSS", http.StatusInternalServerError) ) // Message sends an error with a simple message to the client. -func (e ErrorCodeName) Message(ctx iris.Context, format string, args ...interface{}) { +func (e ErrorCodeName) Message(ctx *context.Context, format string, args ...interface{}) { fail(ctx, e, sprintf(format, args...), "", nil, nil) } // Details sends an error with a message and details to the client. -func (e ErrorCodeName) Details(ctx iris.Context, msg, details string, detailsArgs ...interface{}) { +func (e ErrorCodeName) Details(ctx *context.Context, msg, details string, detailsArgs ...interface{}) { fail(ctx, e, msg, sprintf(details, detailsArgs...), nil, nil) } // Data sends an error with a message and json data to the client. -func (e ErrorCodeName) Data(ctx iris.Context, msg string, data interface{}) { +func (e ErrorCodeName) Data(ctx *context.Context, msg string, data interface{}) { fail(ctx, e, msg, "", nil, data) } // DataWithDetails sends an error with a message, details and json data to the client. -func (e ErrorCodeName) DataWithDetails(ctx iris.Context, msg, details string, data interface{}) { +func (e ErrorCodeName) DataWithDetails(ctx *context.Context, msg, details string, data interface{}) { fail(ctx, e, msg, details, nil, data) } // Validation sends an error which renders the invalid fields to the client. -func (e ErrorCodeName) Validation(ctx iris.Context, validationErrors ...ValidationError) { +func (e ErrorCodeName) Validation(ctx *context.Context, validationErrors ...ValidationError) { e.validation(ctx, validationErrors) } -func (e ErrorCodeName) validation(ctx iris.Context, validationErrors interface{}) { +func (e ErrorCodeName) validation(ctx *context.Context, validationErrors interface{}) { fail(ctx, e, "validation failure", "fields were invalid", validationErrors, nil) } // Err sends the error's text as a message to the client. // In exception, if the given "err" is a type of validation error // then the Validation method is called instead. -func (e ErrorCodeName) Err(ctx iris.Context, err error) { +func (e ErrorCodeName) Err(ctx *context.Context, err error) { if err == nil { return } @@ -163,7 +164,7 @@ func (e ErrorCodeName) Err(ctx iris.Context, err error) { // error using the "LogError" package-level function, which can be customized. // // See "LogErr" too. -func (e ErrorCodeName) Log(ctx iris.Context, format string, args ...interface{}) { +func (e ErrorCodeName) Log(ctx *context.Context, format string, args ...interface{}) { if SkipCanceled { if ctx.IsCanceled() { return @@ -171,7 +172,7 @@ func (e ErrorCodeName) Log(ctx iris.Context, format string, args ...interface{}) for _, arg := range args { if err, ok := arg.(error); ok { - if iris.IsErrCanceled(err) { + if context.IsErrCanceled(err) { return } } @@ -184,8 +185,8 @@ func (e ErrorCodeName) Log(ctx iris.Context, format string, args ...interface{}) // LogErr sends the given "err" as message to the client and prints that // error to using the "LogError" package-level function, which can be customized. -func (e ErrorCodeName) LogErr(ctx iris.Context, err error) { - if SkipCanceled && (ctx.IsCanceled() || iris.IsErrCanceled(err)) { +func (e ErrorCodeName) LogErr(ctx *context.Context, err error) { + if SkipCanceled && (ctx.IsCanceled() || context.IsErrCanceled(err)) { return } @@ -204,7 +205,7 @@ func (e ErrorCodeName) LogErr(ctx iris.Context, err error) { // the error will be sent using the "Internal.LogErr" method which sends // HTTP internal server error to the end-client and // prints the "err" using the "LogError" package-level function. -func HandleAPIError(ctx iris.Context, err error) { +func HandleAPIError(ctx *context.Context, err error) { // Error expected and came from the external server, // save its body so we can forward it to the end-client. if apiErr, ok := client.GetError(err); ok { @@ -228,7 +229,7 @@ var ( // The server fails to send an error on two cases: // 1. when the provided error code name is not registered (the error value is the ErrUnexpectedErrorCode) // 2. when the error contains data but cannot be encoded to json (the value of the error is the result error of json.Marshal). - ErrUnexpected = E("UNEXPECTED_ERROR", iris.StatusInternalServerError) + ErrUnexpected = E("UNEXPECTED_ERROR", http.StatusInternalServerError) // ErrUnexpectedErrorCode is the error which logged // when the given error code name is not registered. ErrUnexpectedErrorCode = New("unexpected error code name") @@ -261,13 +262,13 @@ func (err Error) Error() string { } if err.ErrorCode.Status <= 0 { - err.ErrorCode.Status = iris.StatusInternalServerError + err.ErrorCode.Status = http.StatusInternalServerError } return sprintf("iris http wire error: canonical name: %s, http status code: %d, message: %s, details: %s", err.ErrorCode.CanonicalName, err.ErrorCode.Status, err.Message, err.Details) } -func fail(ctx iris.Context, codeName ErrorCodeName, msg, details string, validationErrors interface{}, dataValue interface{}) { +func fail(ctx *context.Context, codeName ErrorCodeName, msg, details string, validationErrors interface{}, dataValue interface{}) { errorCode, ok := errorCodeMap[codeName] if !ok { // This SHOULD NEVER happen, all ErrorCodeNames MUST be registered. From 84d1a6033492ba3d705a8772e99a4e5bfd2b8a3e Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Thu, 31 Mar 2022 20:47:01 +0300 Subject: [PATCH 02/20] add Context.SetJSONOptions --- HISTORY.md | 3 +++ context/context.go | 57 ++++++++++++++++++++++++++++++++++++++++------ x/errors/errors.go | 4 ++-- 3 files changed, 55 insertions(+), 9 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index 113350c1..63e3d7e6 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -28,6 +28,9 @@ The codebase for Dependency Injection, Internationalization and localization and ## Fixes and Improvements +- Add `Context.SetJSONOptions` to customize on a higher level the JSON options on `Context.JSON` calls. +- Add new `sso` sub-package which helps on any user type auth using JWT (access & refresh tokens) and a cookie (optional). + - Add `Party.EnsureStaticBindings` which, if called, the MVC binder panics if a struct's input binding depends on the HTTP request data instead of a static dependency. This is useful to make sure your API crafted through `Party.PartyConfigure` depends only on struct values you already defined at `Party.RegisterDependency` == will never use reflection at serve-time (maximum performance). - Add a new [x/sqlx](/x/sqlx/) sub-package ([example](_examples/database/sqlx/main.go)). diff --git a/context/context.go b/context/context.go index e1944f4e..9d87a409 100644 --- a/context/context.go +++ b/context/context.go @@ -3761,6 +3761,16 @@ type JSON struct { // Optional context cancelation of encoder when Iris optimizations field is enabled. // On JSON method this is automatically binded to the request context. Context stdContext.Context + + // ErrorHandler can be optionally registered to fire a customized + // error to the client on JSON write failures. + ErrorHandler ErrorHandler +} + +// ErrorHandler describes a context error handler. As for today this is only used +// to globally or per-party or per-route handle JSON writes error. +type ErrorHandler interface { + HandleContextError(ctx *Context, err error) } // IsDefault reports whether this JSON options structure holds the default values. @@ -3771,7 +3781,8 @@ func (j *JSON) IsDefault() bool { j.Prefix == DefaultJSONOptions.Prefix && j.ASCII == DefaultJSONOptions.ASCII && j.Secure == DefaultJSONOptions.Secure && - j.Proto == DefaultJSONOptions.Proto + j.Proto == DefaultJSONOptions.Proto && + j.ErrorHandler == nil } // GetContext returns the option's Context or the HTTP request's one. @@ -3942,18 +3953,55 @@ func stringToBytes(s string) []byte { // inside `ctx.JSON`. var DefaultJSONOptions = JSON{} +const jsonOptionsContextKey = "iris.context.json_options" + +// SetJSONOptions stores the given JSON options to the handler +// for any next Context.JSON calls. Note that the Context.JSON's +// variadic options have priority over these given options. +func (ctx *Context) SetJSONOptions(opts JSON) { + ctx.values.Set(jsonOptionsContextKey, opts) +} + +func (ctx *Context) getJSONOptions() (JSON, bool) { + if v := ctx.values.Get(jsonOptionsContextKey); v != nil { + opts, ok := v.(JSON) + return opts, ok + } + + return DefaultJSONOptions, false +} + // JSON marshals the given interface object and writes the JSON response to the client. // If the value is a compatible `proto.Message` one // then it only uses the options.Proto settings to marshal. func (ctx *Context) JSON(v interface{}, opts ...JSON) (n int, err error) { + if n, err = ctx.writeJSON(v, opts...); err != nil { + if opts, ok := ctx.getJSONOptions(); ok { + opts.ErrorHandler.HandleContextError(ctx, err) + } // keep the error so the caller has control over further actions. + } + + return +} + +func (ctx *Context) writeJSON(v interface{}, opts ...JSON) (n int, err error) { ctx.ContentType(ContentJSONHeaderValue) shouldOptimize := ctx.shouldOptimize() + options := DefaultJSONOptions optsLength := len(opts) + if optsLength > 0 { + options = opts[0] + } else { + if opt, ok := ctx.getJSONOptions(); ok { + opts = []JSON{opt} + optsLength = 1 + } + } if shouldOptimize && optsLength == 0 { // if no options given and optimizations are enabled. // try handle proto or easyjson. - if handled, n, err := handleJSONResponseValue(ctx, v, DefaultJSONOptions); handled { + if handled, n, err := handleJSONResponseValue(ctx, v, options); handled { return n, err } @@ -3966,11 +4014,6 @@ func (ctx *Context) JSON(v interface{}, opts ...JSON) (n int, err error) { return ctx.Write(result) } - options := DefaultJSONOptions - if optsLength > 0 { - options = opts[0] - } - if options.StreamingJSON { if shouldOptimize { // jsoniterConfig := jsoniter.Config{ diff --git a/x/errors/errors.go b/x/errors/errors.go index be9ee8c3..2ef2438a 100644 --- a/x/errors/errors.go +++ b/x/errors/errors.go @@ -248,7 +248,7 @@ type Error struct { } // Error method completes the error interface. It just returns the canonical name, status code, message and details. -func (err Error) Error() string { +func (err *Error) Error() string { if err.Message == "" { err.Message = "" } @@ -312,6 +312,6 @@ func fail(ctx *context.Context, codeName ErrorCodeName, msg, details string, val Validation: validationErrors, } - // ctx.SetErr(err) + // ctx.SetErr(&err) ctx.StopWithJSON(errorCode.Status, err) } From 4f81478ad652daf6bb84d1813a6f751299a49538 Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Thu, 31 Mar 2022 20:57:05 +0300 Subject: [PATCH 03/20] Context.JSONOptions: add usage godoc example --- context/context.go | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/context/context.go b/context/context.go index 9d87a409..31a5449e 100644 --- a/context/context.go +++ b/context/context.go @@ -3781,8 +3781,8 @@ func (j *JSON) IsDefault() bool { j.Prefix == DefaultJSONOptions.Prefix && j.ASCII == DefaultJSONOptions.ASCII && j.Secure == DefaultJSONOptions.Secure && - j.Proto == DefaultJSONOptions.Proto && - j.ErrorHandler == nil + j.Proto == DefaultJSONOptions.Proto + // except context and error handler } // GetContext returns the option's Context or the HTTP request's one. @@ -3958,6 +3958,20 @@ const jsonOptionsContextKey = "iris.context.json_options" // SetJSONOptions stores the given JSON options to the handler // for any next Context.JSON calls. Note that the Context.JSON's // variadic options have priority over these given options. +// +// Usage Example: +// +// type jsonErrorHandler struct{} +// func (e *jsonErrorHandler) HandleContextError(ctx iris.Context, err error) { +// errors.InvalidArgument.Err(ctx, err) +// } +// ... +// errHandler := new(jsonErrorHandler) +// srv.Use(func(ctx iris.Context) { +// ctx.SetJSONOptions(iris.JSON{ +// ErrorHandler: errHandler, +// }) +// }) func (ctx *Context) SetJSONOptions(opts JSON) { ctx.values.Set(jsonOptionsContextKey, opts) } @@ -3994,8 +4008,10 @@ func (ctx *Context) writeJSON(v interface{}, opts ...JSON) (n int, err error) { options = opts[0] } else { if opt, ok := ctx.getJSONOptions(); ok { - opts = []JSON{opt} - optsLength = 1 + options = opt + if !options.IsDefault() { // keep the next branch valid when only the Context or/and ErrorHandler are modified. + optsLength = 1 + } } } From 60e19de9e25852ee5e8bd3f18c25bbada53bb29b Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Thu, 31 Mar 2022 20:58:40 +0300 Subject: [PATCH 04/20] minor --- context/context.go | 1 + 1 file changed, 1 insertion(+) diff --git a/context/context.go b/context/context.go index 31a5449e..030f41b8 100644 --- a/context/context.go +++ b/context/context.go @@ -3971,6 +3971,7 @@ const jsonOptionsContextKey = "iris.context.json_options" // ctx.SetJSONOptions(iris.JSON{ // ErrorHandler: errHandler, // }) +// ctx.Next() // }) func (ctx *Context) SetJSONOptions(opts JSON) { ctx.values.Set(jsonOptionsContextKey, opts) From 8652ee09f6a894cd7f22ea2d157868f0c2ef4981 Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Sat, 2 Apr 2022 17:30:55 +0300 Subject: [PATCH 05/20] rename the sso to auth package --- HISTORY.md | 2 +- _examples/README.md | 4 +- _examples/auth/{sso => auth}/README.md | 2 +- .../sso.yml => auth/auth/auth.yml} | 9 +- _examples/auth/{sso => auth}/main.go | 42 ++++---- _examples/auth/{sso => auth}/user.go | 0 _examples/auth/{sso => auth}/user_provider.go | 6 +- .../{sso => auth}/views/layouts/main.html | 0 .../{sso => auth}/views/partials/footer.html | 0 .../auth/{sso => auth}/views/signin.html | 0 _examples/mvc/login/services/user_service.go | 2 +- .../sso.yml => mvc/websocket-auth/auth.yml} | 9 +- .../browser/index.html | 0 .../{websocket-sso => websocket-auth}/main.go | 6 +- .../{websocket-sso => websocket-auth}/user.go | 0 .../user_provider.go | 6 +- .../views/layouts/main.html | 0 .../views/partials/footer.html | 0 .../views/signin.html | 0 sso/sso.go => auth/auth.go | 102 +++++++----------- {sso => auth}/configuration.go | 67 +++++++++--- auth/provider.go | 86 +++++++++++++++ {sso => auth}/user.go | 12 +-- sso/provider.go | 83 -------------- 24 files changed, 233 insertions(+), 205 deletions(-) rename _examples/auth/{sso => auth}/README.md (74%) rename _examples/{mvc/websocket-sso/sso.yml => auth/auth/auth.yml} (85%) rename _examples/auth/{sso => auth}/main.go (75%) rename _examples/auth/{sso => auth}/user.go (100%) rename _examples/auth/{sso => auth}/user_provider.go (92%) rename _examples/auth/{sso => auth}/views/layouts/main.html (100%) rename _examples/auth/{sso => auth}/views/partials/footer.html (100%) rename _examples/auth/{sso => auth}/views/signin.html (100%) rename _examples/{auth/sso/sso.yml => mvc/websocket-auth/auth.yml} (85%) rename _examples/mvc/{websocket-sso => websocket-auth}/browser/index.html (100%) rename _examples/mvc/{websocket-sso => websocket-auth}/main.go (93%) rename _examples/mvc/{websocket-sso => websocket-auth}/user.go (100%) rename _examples/mvc/{websocket-sso => websocket-auth}/user_provider.go (92%) rename _examples/mvc/{websocket-sso => websocket-auth}/views/layouts/main.html (100%) rename _examples/mvc/{websocket-sso => websocket-auth}/views/partials/footer.html (100%) rename _examples/mvc/{websocket-sso => websocket-auth}/views/signin.html (100%) rename sso/sso.go => auth/auth.go (77%) rename {sso => auth}/configuration.go (53%) create mode 100644 auth/provider.go rename {sso => auth}/user.go (69%) delete mode 100644 sso/provider.go diff --git a/HISTORY.md b/HISTORY.md index 63e3d7e6..4b08299a 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -29,7 +29,7 @@ The codebase for Dependency Injection, Internationalization and localization and ## Fixes and Improvements - Add `Context.SetJSONOptions` to customize on a higher level the JSON options on `Context.JSON` calls. -- Add new `sso` sub-package which helps on any user type auth using JWT (access & refresh tokens) and a cookie (optional). +- Add new [auth](auth) sub-package which helps on any user type auth using JWT (access & refresh tokens) and a cookie (optional). - Add `Party.EnsureStaticBindings` which, if called, the MVC binder panics if a struct's input binding depends on the HTTP request data instead of a static dependency. This is useful to make sure your API crafted through `Party.PartyConfigure` depends only on struct values you already defined at `Party.RegisterDependency` == will never use reflection at serve-time (maximum performance). diff --git a/_examples/README.md b/_examples/README.md index 78905cd2..8a7477e1 100644 --- a/_examples/README.md +++ b/_examples/README.md @@ -214,6 +214,7 @@ * [Ttemplates and Functions](i18n/template) * [Pluralization and Variables](i18n/plurals) * Authentication, Authorization & Bot Detection + * [Recommended: Auth package and Single-Sign-On](auth/auth) **NEW (GO 1.18 Generics required)** * Basic Authentication * [Basic](auth/basicauth/basic) * [Load from a slice of Users](auth/basicauth/users_list) @@ -226,7 +227,6 @@ * [Blocklist](auth/jwt/blocklist/main.go) * [Refresh Token](auth/jwt/refresh-token/main.go) * [Tutorial](auth/jwt/tutorial) - * [SSO](auth/sso) **NEW (GO 1.18 Generics required)** * [JWT (community edition)](https://github.com/iris-contrib/middleware/tree/v12/jwt/_example/main.go) * [OAUth2](auth/goth/main.go) * [Manage Permissions](auth/permissions/main.go) @@ -279,7 +279,7 @@ * [Authenticated Controller](mvc/authenticated-controller/main.go) * [Versioned Controller](mvc/versioned-controller/main.go) * [Websocket Controller](mvc/websocket) - * [Websocket + Authentication (SSO)](mvc/websocket-sso) **NEW (GO 1.18 Generics required)** + * [Websocket + Authentication (Single-Sign-On)](mvc/websocket-auth) **NEW (GO 1.18 Generics required)** * [Register Middleware](mvc/middleware) * [gRPC](mvc/grpc-compatible) * [gRPC Bidirectional Stream](mvc/grpc-compatible-bidirectional-stream) diff --git a/_examples/auth/sso/README.md b/_examples/auth/auth/README.md similarity index 74% rename from _examples/auth/sso/README.md rename to _examples/auth/auth/README.md index ffa7a823..3f91efeb 100644 --- a/_examples/auth/sso/README.md +++ b/_examples/auth/auth/README.md @@ -1,4 +1,4 @@ -# SSO (Single Sign On) +# Auth Package (+ Single Sign On) ```sh $ go run . diff --git a/_examples/mvc/websocket-sso/sso.yml b/_examples/auth/auth/auth.yml similarity index 85% rename from _examples/mvc/websocket-sso/sso.yml rename to _examples/auth/auth/auth.yml index c25a22bc..e324132f 100644 --- a/_examples/mvc/websocket-sso/sso.yml +++ b/_examples/auth/auth/auth.yml @@ -1,9 +1,12 @@ +Headers: # required. + - "Authorization" + - "X-Authorization" Cookie: # optional. - Name: "iris_sso" + Name: "iris_auth_cookie" Hash: "D*G-KaPdSgUkXp2s5v8y/B?E(H+MbQeThWmYq3t6w9z$C&F)J@NcRfUjXn2r4u7x" # length of 64 characters (512-bit). Block: "VkYp3s6v9y$B&E)H@McQfTjWmZq4t7w!" # length of 32 characters (256-bit). Keys: - - ID: IRIS_SSO_ACCESS # required. + - ID: IRIS_AUTH_ACCESS # required. Alg: EdDSA MaxAge: 2h # 2 hours lifetime for access tokens. Private: |+ @@ -14,7 +17,7 @@ Keys: -----BEGIN PUBLIC KEY----- MCowBQYDK2VwAyEAzpgjKSr9E032DX+foiOxq1QDsbzjLxagTN+yVpGWZB4= -----END PUBLIC KEY----- - - ID: IRIS_SSO_REFRESH # optional. Good practise to have it though. + - ID: IRIS_AUTH_REFRESH # optional. Good practise to have it though. Alg: EdDSA # 1 month lifetime for refresh tokens, # after that period the user has to signin again. diff --git a/_examples/auth/sso/main.go b/_examples/auth/auth/main.go similarity index 75% rename from _examples/auth/sso/main.go rename to _examples/auth/auth/main.go index f09236a2..110948e7 100644 --- a/_examples/auth/sso/main.go +++ b/_examples/auth/auth/main.go @@ -6,10 +6,10 @@ import ( "fmt" "github.com/kataras/iris/v12" - "github.com/kataras/iris/v12/sso" + "github.com/kataras/iris/v12/auth" ) -func allowRole(role AccessRole) sso.TVerify[User] { +func allowRole(role AccessRole) auth.TVerify[User] { return func(u User) error { if !u.Role.Allow(role) { return fmt.Errorf("invalid role") @@ -19,7 +19,7 @@ func allowRole(role AccessRole) sso.TVerify[User] { } } -const configFilename = "./sso.yml" +const configFilename = "./auth.yml" func main() { app := iris.New() @@ -28,23 +28,23 @@ func main() { Layout("main")) /* - // Easiest 1-liner way, load from configuration and initialize a new sso instance: - s := sso.MustLoad[User]("./sso.yml") + // Easiest 1-liner way, load from configuration and initialize a new auth instance: + s := auth.MustLoad[User]("./auth.yml") // Bind a configuration from file: - var c sso.Configuration - c.BindFile("./sso.yml") - s, err := sso.New[User](c) + var c auth.Configuration + c.BindFile("./auth.yml") + s, err := auth.New[User](c) // OR create new programmatically configuration: - config := sso.Configuration{ + config := auth.Configuration{ ...fields } - s, err := sso.New[User](config) + s, err := auth.New[User](config) // OR generate a new configuration: - config := sso.MustGenerateConfiguration() - s, err := sso.New[User](config) + config := auth.MustGenerateConfiguration() + s, err := auth.New[User](config) // OR generate a new config and save it if cannot open the config file. if _, err := os.Stat(configFilename); err != nil { - generatedConfig := sso.MustGenerateConfiguration() + generatedConfig := auth.MustGenerateConfiguration() configContents, err := generatedConfig.ToYAML() if err != nil { panic(err) @@ -58,13 +58,13 @@ func main() { */ // 1. Load configuration from a file. - ssoConfig, err := sso.LoadConfiguration(configFilename) + authConfig, err := auth.LoadConfiguration(configFilename) if err != nil { panic(err) } - // 2. Initialize a new sso instance for "User" claims (generics: go1.18 +). - s, err := sso.New[User](ssoConfig) + // 2. Initialize a new auth instance for "User" claims (generics: go1.18 +). + s, err := auth.New[User](authConfig) if err != nil { panic(err) } @@ -72,7 +72,7 @@ func main() { // 3. Add a custom provider, in our case is just a memory-based one. s.AddProvider(NewProvider()) // 3.1. Optionally set a custom error handler. - // s.SetErrorHandler(new(sso.DefaultErrorHandler)) + // s.SetErrorHandler(new(auth.DefaultErrorHandler)) app.Get("/signin", renderSigninForm) // 4. generate token pairs. @@ -102,12 +102,12 @@ func main() { Region: "us", Tunnels: []tunnel.Tunnel{ { - Name: "Iris SSO (Test)", + Name: "Iris Auth (Test)", Addr: ":8080", Hostname: "YOUR_DOMAIN", }, { - Name: "Iris SSO (Test Subdomain)", + Name: "Iris Auth (Test Subdomain)", Addr: ":8080", Hostname: "owner.YOUR_DOMAIN", }, @@ -120,14 +120,14 @@ func renderSigninForm(ctx iris.Context) { ctx.View("signin", iris.Map{"Title": "Signin Page"}) } -func renderMemberPage(s *sso.SSO[User]) iris.Handler { +func renderMemberPage(s *auth.Auth[User]) iris.Handler { return func(ctx iris.Context) { user := s.GetUser(ctx) ctx.Writef("Hello member: %s\n", user.Email) } } -func renderOwnerPage(s *sso.SSO[User]) iris.Handler { +func renderOwnerPage(s *auth.Auth[User]) iris.Handler { return func(ctx iris.Context) { user := s.GetUser(ctx) ctx.Writef("Hello owner: %s\n", user.Email) diff --git a/_examples/auth/sso/user.go b/_examples/auth/auth/user.go similarity index 100% rename from _examples/auth/sso/user.go rename to _examples/auth/auth/user.go diff --git a/_examples/auth/sso/user_provider.go b/_examples/auth/auth/user_provider.go similarity index 92% rename from _examples/auth/sso/user_provider.go rename to _examples/auth/auth/user_provider.go index 151e426b..aedbc10a 100644 --- a/_examples/auth/sso/user_provider.go +++ b/_examples/auth/auth/user_provider.go @@ -8,7 +8,7 @@ import ( "sync" "time" - "github.com/kataras/iris/v12/sso" + "github.com/kataras/iris/v12/auth" ) type Provider struct { @@ -49,7 +49,7 @@ func (p *Provider) Signin(ctx context.Context, username, password string) (User, return User{}, fmt.Errorf("user not found") } -func (p *Provider) ValidateToken(ctx context.Context, standardClaims sso.StandardClaims, u User) error { // fired on VerifyHandler. +func (p *Provider) ValidateToken(ctx context.Context, standardClaims auth.StandardClaims, u User) error { // fired on VerifyHandler. // your database and checks of blocked tokens... // check for specific token ids. @@ -81,7 +81,7 @@ func (p *Provider) ValidateToken(ctx context.Context, standardClaims sso.Standar return nil // else valid. } -func (p *Provider) InvalidateToken(ctx context.Context, standardClaims sso.StandardClaims, u User) error { // fired on SignoutHandler. +func (p *Provider) InvalidateToken(ctx context.Context, standardClaims auth.StandardClaims, u User) error { // fired on SignoutHandler. // invalidate this specific token. p.mu.Lock() p.invalidated[standardClaims.ID] = struct{}{} diff --git a/_examples/auth/sso/views/layouts/main.html b/_examples/auth/auth/views/layouts/main.html similarity index 100% rename from _examples/auth/sso/views/layouts/main.html rename to _examples/auth/auth/views/layouts/main.html diff --git a/_examples/auth/sso/views/partials/footer.html b/_examples/auth/auth/views/partials/footer.html similarity index 100% rename from _examples/auth/sso/views/partials/footer.html rename to _examples/auth/auth/views/partials/footer.html diff --git a/_examples/auth/sso/views/signin.html b/_examples/auth/auth/views/signin.html similarity index 100% rename from _examples/auth/sso/views/signin.html rename to _examples/auth/auth/views/signin.html diff --git a/_examples/mvc/login/services/user_service.go b/_examples/mvc/login/services/user_service.go index 4921c4f9..32ebf5c7 100644 --- a/_examples/mvc/login/services/user_service.go +++ b/_examples/mvc/login/services/user_service.go @@ -51,7 +51,7 @@ func (s *userService) GetByID(id int64) (datamodels.User, bool) { }) } -// GetByUsernameAndPassword returns a user based on its username and passowrd, +// GetByUsernameAndPassword returns a user based on its username and password, // used for authentication. func (s *userService) GetByUsernameAndPassword(username, userPassword string) (datamodels.User, bool) { if username == "" || userPassword == "" { diff --git a/_examples/auth/sso/sso.yml b/_examples/mvc/websocket-auth/auth.yml similarity index 85% rename from _examples/auth/sso/sso.yml rename to _examples/mvc/websocket-auth/auth.yml index c25a22bc..e324132f 100644 --- a/_examples/auth/sso/sso.yml +++ b/_examples/mvc/websocket-auth/auth.yml @@ -1,9 +1,12 @@ +Headers: # required. + - "Authorization" + - "X-Authorization" Cookie: # optional. - Name: "iris_sso" + Name: "iris_auth_cookie" Hash: "D*G-KaPdSgUkXp2s5v8y/B?E(H+MbQeThWmYq3t6w9z$C&F)J@NcRfUjXn2r4u7x" # length of 64 characters (512-bit). Block: "VkYp3s6v9y$B&E)H@McQfTjWmZq4t7w!" # length of 32 characters (256-bit). Keys: - - ID: IRIS_SSO_ACCESS # required. + - ID: IRIS_AUTH_ACCESS # required. Alg: EdDSA MaxAge: 2h # 2 hours lifetime for access tokens. Private: |+ @@ -14,7 +17,7 @@ Keys: -----BEGIN PUBLIC KEY----- MCowBQYDK2VwAyEAzpgjKSr9E032DX+foiOxq1QDsbzjLxagTN+yVpGWZB4= -----END PUBLIC KEY----- - - ID: IRIS_SSO_REFRESH # optional. Good practise to have it though. + - ID: IRIS_AUTH_REFRESH # optional. Good practise to have it though. Alg: EdDSA # 1 month lifetime for refresh tokens, # after that period the user has to signin again. diff --git a/_examples/mvc/websocket-sso/browser/index.html b/_examples/mvc/websocket-auth/browser/index.html similarity index 100% rename from _examples/mvc/websocket-sso/browser/index.html rename to _examples/mvc/websocket-auth/browser/index.html diff --git a/_examples/mvc/websocket-sso/main.go b/_examples/mvc/websocket-auth/main.go similarity index 93% rename from _examples/mvc/websocket-sso/main.go rename to _examples/mvc/websocket-auth/main.go index 1e59e264..89a52d93 100644 --- a/_examples/mvc/websocket-sso/main.go +++ b/_examples/mvc/websocket-auth/main.go @@ -6,8 +6,8 @@ import ( "fmt" "github.com/kataras/iris/v12" + "github.com/kataras/iris/v12/auth" "github.com/kataras/iris/v12/mvc" - "github.com/kataras/iris/v12/sso" "github.com/kataras/iris/v12/websocket" ) @@ -29,7 +29,7 @@ func newApp() *iris.Application { LayoutDir("layouts"). Layout("main")) - s := sso.MustLoad[User]("./sso.yml") + s := auth.MustLoad[User]("./auth.yml") s.AddProvider(NewProvider()) app.Get("/signin", renderSigninForm) @@ -63,7 +63,7 @@ func (c *websocketController) Namespace() string { func (c *websocketController) OnChat(msg websocket.Message) error { ctx := websocket.GetContext(c.Conn) - user := sso.GetUser[User](ctx) + user := auth.GetUser[User](ctx) msg.Body = []byte(fmt.Sprintf("%s: %s", user.Email, string(msg.Body))) c.Conn.Server().Broadcast(c, msg) diff --git a/_examples/mvc/websocket-sso/user.go b/_examples/mvc/websocket-auth/user.go similarity index 100% rename from _examples/mvc/websocket-sso/user.go rename to _examples/mvc/websocket-auth/user.go diff --git a/_examples/mvc/websocket-sso/user_provider.go b/_examples/mvc/websocket-auth/user_provider.go similarity index 92% rename from _examples/mvc/websocket-sso/user_provider.go rename to _examples/mvc/websocket-auth/user_provider.go index a1846c15..16fbd7ce 100644 --- a/_examples/mvc/websocket-sso/user_provider.go +++ b/_examples/mvc/websocket-auth/user_provider.go @@ -8,7 +8,7 @@ import ( "sync" "time" - "github.com/kataras/iris/v12/sso" + "github.com/kataras/iris/v12/auth" ) type Provider struct { @@ -49,7 +49,7 @@ func (p *Provider) Signin(ctx context.Context, username, password string) (User, return User{}, fmt.Errorf("user not found") } -func (p *Provider) ValidateToken(ctx context.Context, standardClaims sso.StandardClaims, u User) error { // fired on VerifyHandler. +func (p *Provider) ValidateToken(ctx context.Context, standardClaims auth.StandardClaims, u User) error { // fired on VerifyHandler. // your database and checks of blocked tokens... // check for specific token ids. @@ -81,7 +81,7 @@ func (p *Provider) ValidateToken(ctx context.Context, standardClaims sso.Standar return nil // else valid. } -func (p *Provider) InvalidateToken(ctx context.Context, standardClaims sso.StandardClaims, u User) error { // fired on SignoutHandler. +func (p *Provider) InvalidateToken(ctx context.Context, standardClaims auth.StandardClaims, u User) error { // fired on SignoutHandler. // invalidate this specific token. p.mu.Lock() p.invalidated[standardClaims.ID] = struct{}{} diff --git a/_examples/mvc/websocket-sso/views/layouts/main.html b/_examples/mvc/websocket-auth/views/layouts/main.html similarity index 100% rename from _examples/mvc/websocket-sso/views/layouts/main.html rename to _examples/mvc/websocket-auth/views/layouts/main.html diff --git a/_examples/mvc/websocket-sso/views/partials/footer.html b/_examples/mvc/websocket-auth/views/partials/footer.html similarity index 100% rename from _examples/mvc/websocket-sso/views/partials/footer.html rename to _examples/mvc/websocket-auth/views/partials/footer.html diff --git a/_examples/mvc/websocket-sso/views/signin.html b/_examples/mvc/websocket-auth/views/signin.html similarity index 100% rename from _examples/mvc/websocket-sso/views/signin.html rename to _examples/mvc/websocket-auth/views/signin.html diff --git a/sso/sso.go b/auth/auth.go similarity index 77% rename from sso/sso.go rename to auth/auth.go index 047b4756..3e8d405d 100644 --- a/sso/sso.go +++ b/auth/auth.go @@ -1,6 +1,6 @@ //go:build go1.18 -package sso +package auth import ( stdContext "context" @@ -18,7 +18,7 @@ import ( ) type ( - SSO[T User] struct { + Auth[T User] struct { config Configuration keys jwt.Keys @@ -49,7 +49,7 @@ type ( } ) -func MustLoad[T User](filename string) *SSO[T] { +func MustLoad[T User](filename string) *Auth[T] { var config Configuration if err := config.BindFile(filename); err != nil { panic(err) @@ -63,7 +63,7 @@ func MustLoad[T User](filename string) *SSO[T] { return s } -func Must[T User](s *SSO[T], err error) *SSO[T] { +func Must[T User](s *Auth[T], err error) *Auth[T] { if err != nil { panic(err) } @@ -71,14 +71,14 @@ func Must[T User](s *SSO[T], err error) *SSO[T] { return s } -func New[T User](config Configuration) (*SSO[T], error) { +func New[T User](config Configuration) (*Auth[T], error) { keys, err := config.validate() if err != nil { return nil, err } _, refreshEnabled := keys[KIDRefresh] - s := &SSO[T]{ + s := &Auth[T]{ config: config, keys: keys, securecookie: securecookie.New([]byte(config.Cookie.Hash), []byte(config.Cookie.Block)), @@ -90,7 +90,7 @@ func New[T User](config Configuration) (*SSO[T], error) { return s, nil } -func (s *SSO[T]) WithProviderAndErrorHandler(provider Provider[T], errHandler ErrorHandler) *SSO[T] { +func (s *Auth[T]) WithProviderAndErrorHandler(provider Provider[T], errHandler ErrorHandler) *Auth[T] { if provider != nil { for i := range s.providers { s.providers[i] = nil @@ -108,11 +108,7 @@ func (s *SSO[T]) WithProviderAndErrorHandler(provider Provider[T], errHandler Er return s } -func (s *SSO[T]) AddProvider(providers ...Provider[T]) *SSO[T] { - // defaultProviderTypename := strings.Replace(fmt.Sprintf("%T", s), "SSO", "provider", 1) - // if len(s.providers) == 1 && fmt.Sprintf("%T", s.providers[0]) == defaultProviderTypename { - // s.providers = append(s.providers[1:], p...) - +func (s *Auth[T]) AddProvider(providers ...Provider[T]) *Auth[T] { // A provider can also implement both transformer and // error handler if that's the design option of the end-developer. for _, p := range providers { @@ -137,22 +133,22 @@ func (s *SSO[T]) AddProvider(providers ...Provider[T]) *SSO[T] { return s } -func (s *SSO[T]) SetErrorHandler(errHandler ErrorHandler) *SSO[T] { +func (s *Auth[T]) SetErrorHandler(errHandler ErrorHandler) *Auth[T] { s.errorHandler = errHandler return s } -func (s *SSO[T]) SetTransformer(transformer Transformer[T]) *SSO[T] { +func (s *Auth[T]) SetTransformer(transformer Transformer[T]) *Auth[T] { s.transformer = transformer return s } -func (s *SSO[T]) SetTransformerFunc(transfermerFunc func(ctx stdContext.Context, tok *VerifiedToken) (T, error)) *SSO[T] { +func (s *Auth[T]) SetTransformerFunc(transfermerFunc func(ctx stdContext.Context, tok *VerifiedToken) (T, error)) *Auth[T] { s.transformer = TransformerFunc[T](transfermerFunc) return s } -func (s *SSO[T]) Signin(ctx stdContext.Context, username, password string) ([]byte, []byte, error) { +func (s *Auth[T]) Signin(ctx stdContext.Context, username, password string) ([]byte, []byte, error) { var t T // get "t" from a valid provider. @@ -163,7 +159,7 @@ func (s *SSO[T]) Signin(ctx stdContext.Context, username, password string) ([]by v, err := p.Signin(ctx, username, password) if err != nil { if i == n-1 { // last provider errored. - return nil, nil, fmt.Errorf("sso: signin: %w", err) + return nil, nil, fmt.Errorf("auth: signin: %w", err) } // keep searching. continue @@ -174,19 +170,19 @@ func (s *SSO[T]) Signin(ctx stdContext.Context, username, password string) ([]by break } } else { - return nil, nil, fmt.Errorf("sso: signin: no provider") + return nil, nil, fmt.Errorf("auth: signin: no provider") } // sign the tokens. accessToken, refreshToken, err := s.sign(t) if err != nil { - return nil, nil, fmt.Errorf("sso: signin: %w", err) + return nil, nil, fmt.Errorf("auth: signin: %w", err) } return accessToken, refreshToken, nil } -func (s *SSO[T]) sign(t T) ([]byte, []byte, error) { +func (s *Auth[T]) sign(t T) ([]byte, []byte, error) { // sign the tokens. var ( accessStdClaims StandardClaims @@ -239,7 +235,7 @@ func (s *SSO[T]) sign(t T) ([]byte, []byte, error) { return accessToken, refreshToken, nil } -func (s *SSO[T]) SigninHandler(ctx *context.Context) { +func (s *Auth[T]) SigninHandler(ctx *context.Context) { // No, let the developer decide it based on a middleware, e.g. iris.LimitRequestBodySize. // ctx.SetMaxRequestBodySize(s.maxRequestBodySize) @@ -283,16 +279,16 @@ func (s *SSO[T]) SigninHandler(ctx *context.Context) { ctx.JSON(resp) } -func (s *SSO[T]) Verify(ctx stdContext.Context, token []byte) (T, StandardClaims, error) { +func (s *Auth[T]) Verify(ctx stdContext.Context, token []byte) (T, StandardClaims, error) { t, claims, err := s.verify(ctx, token) if err != nil { - return t, StandardClaims{}, fmt.Errorf("sso: verify: %w", err) + return t, StandardClaims{}, fmt.Errorf("auth: verify: %w", err) } return t, claims, nil } -func (s *SSO[T]) verify(ctx stdContext.Context, token []byte) (T, StandardClaims, error) { +func (s *Auth[T]) verify(ctx stdContext.Context, token []byte) (T, StandardClaims, error) { var t T if len(token) == 0 { // should never happen at this state. @@ -339,23 +335,7 @@ func (s *SSO[T]) verify(ctx stdContext.Context, token []byte) (T, StandardClaims return t, standardClaims, nil } -/* Good idea but not practical. -func Transform[T User, V User](transformer Transformer[T, V]) context.Handler { - return func(ctx *context.Context) { - existingUserValue := GetUser[T](ctx) - newUserValue, err := transformer.Transform(ctx, existingUserValue) - if err != nil { - ctx.SetErr(err) - return - } - - ctx.Values().Set(userContextKey, newUserValue) - ctx.Next() - } -} -*/ - -func (s *SSO[T]) VerifyHandler(verifyFuncs ...TVerify[T]) context.Handler { +func (s *Auth[T]) VerifyHandler(verifyFuncs ...TVerify[T]) context.Handler { return func(ctx *context.Context) { accessToken := s.extractAccessToken(ctx) @@ -376,7 +356,7 @@ func (s *SSO[T]) VerifyHandler(verifyFuncs ...TVerify[T]) context.Handler { } if err = verify(t); err != nil { - err = fmt.Errorf("sso: verify: %v", err) + err = fmt.Errorf("auth: verify: %v", err) s.errorHandler.Unauthenticated(ctx, err) return } @@ -394,9 +374,9 @@ func (s *SSO[T]) VerifyHandler(verifyFuncs ...TVerify[T]) context.Handler { } } -func (s *SSO[T]) extractAccessToken(ctx *context.Context) string { +func (s *Auth[T]) extractAccessToken(ctx *context.Context) string { // first try from authorization: bearer header. - accessToken := extractTokenFromHeader(ctx) + accessToken := s.extractTokenFromHeader(ctx) // then if no header, try try extract from cookie. if accessToken == "" { @@ -408,27 +388,27 @@ func (s *SSO[T]) extractAccessToken(ctx *context.Context) string { return accessToken } -func (s *SSO[T]) Refresh(ctx stdContext.Context, refreshToken []byte) ([]byte, []byte, error) { +func (s *Auth[T]) Refresh(ctx stdContext.Context, refreshToken []byte) ([]byte, []byte, error) { if !s.refreshEnabled { - return nil, nil, fmt.Errorf("sso: refresh: disabled") + return nil, nil, fmt.Errorf("auth: refresh: disabled") } t, _, err := s.verify(ctx, refreshToken) if err != nil { - return nil, nil, fmt.Errorf("sso: refresh: %w", err) + return nil, nil, fmt.Errorf("auth: refresh: %w", err) } // refresh the tokens, both refresh & access tokens will be renew to prevent // malicious 😈 users that may hold a refresh token. accessTok, refreshTok, err := s.sign(t) if err != nil { - return nil, nil, fmt.Errorf("sso: refresh: %w", err) + return nil, nil, fmt.Errorf("auth: refresh: %w", err) } return accessTok, refreshTok, nil } -func (s *SSO[T]) RefreshHandler(ctx *context.Context) { +func (s *Auth[T]) RefreshHandler(ctx *context.Context) { var req RefreshRequest err := ctx.ReadJSON(&req) if err != nil { @@ -455,10 +435,10 @@ func (s *SSO[T]) RefreshHandler(ctx *context.Context) { ctx.JSON(resp) } -func (s *SSO[T]) Signout(ctx stdContext.Context, token []byte, all bool) error { +func (s *Auth[T]) Signout(ctx stdContext.Context, token []byte, all bool) error { t, standardClaims, err := s.verify(ctx, token) if err != nil { - return fmt.Errorf("sso: signout: verify: %w", err) + return fmt.Errorf("auth: signout: verify: %w", err) } for i, n := 0, len(s.providers)-1; i <= n; i++ { @@ -485,15 +465,15 @@ func (s *SSO[T]) Signout(ctx stdContext.Context, token []byte, all bool) error { return nil } -func (s *SSO[T]) SignoutHandler(ctx *context.Context) { +func (s *Auth[T]) SignoutHandler(ctx *context.Context) { s.signoutHandler(ctx, false) } -func (s *SSO[T]) SignoutAllHandler(ctx *context.Context) { +func (s *Auth[T]) SignoutAllHandler(ctx *context.Context) { s.signoutHandler(ctx, true) } -func (s *SSO[T]) signoutHandler(ctx *context.Context, all bool) { +func (s *Auth[T]) signoutHandler(ctx *context.Context, all bool) { accessToken := s.extractAccessToken(ctx) if accessToken == "" { s.errorHandler.Unauthenticated(ctx, jwt.ErrMissing) @@ -515,13 +495,8 @@ func (s *SSO[T]) signoutHandler(ctx *context.Context, all bool) { ctx.Values().Remove(standardClaimsContextKey) } -var headerKeys = [...]string{ - "Authorization", - "X-Authorization", -} - -func extractTokenFromHeader(ctx *context.Context) string { - for _, headerKey := range headerKeys { +func (s *Auth[T]) extractTokenFromHeader(ctx *context.Context) string { + for _, headerKey := range s.config.Headers { headerValue := ctx.GetHeader(headerKey) if headerValue == "" { continue @@ -539,7 +514,7 @@ func extractTokenFromHeader(ctx *context.Context) string { return "" } -func (s *SSO[T]) trySetCookie(ctx *context.Context, accessToken string) { +func (s *Auth[T]) trySetCookie(ctx *context.Context, accessToken string) { if cookieName := s.config.Cookie.Name; cookieName != "" { maxAge := s.keys[KIDAccess].MaxAge if maxAge == 0 { @@ -551,6 +526,7 @@ func (s *SSO[T]) trySetCookie(ctx *context.Context, accessToken string) { Name: cookieName, Value: url.QueryEscape(accessToken), HttpOnly: true, + Secure: ctx.IsSSL(), Domain: ctx.Domain(), SameSite: http.SameSiteLaxMode, Expires: time.Now().Add(maxAge), @@ -561,7 +537,7 @@ func (s *SSO[T]) trySetCookie(ctx *context.Context, accessToken string) { } } -func (s *SSO[T]) tryRemoveCookie(ctx *context.Context) { +func (s *Auth[T]) tryRemoveCookie(ctx *context.Context) { if cookieName := s.config.Cookie.Name; cookieName != "" { ctx.RemoveCookie(cookieName) } diff --git a/sso/configuration.go b/auth/configuration.go similarity index 53% rename from sso/configuration.go rename to auth/configuration.go index e3100788..c5200541 100644 --- a/sso/configuration.go +++ b/auth/configuration.go @@ -1,6 +1,6 @@ //go:build go1.18 -package sso +package auth import ( "encoding/json" @@ -16,50 +16,77 @@ import ( ) const ( - KIDAccess = "IRIS_SSO_ACCESS" - KIDRefresh = "IRIS_SSO_REFRESH" + // The JWT Key ID for access tokens. + KIDAccess = "IRIS_AUTH_ACCESS" + // The JWT Key ID for refresh tokens. + KIDRefresh = "IRIS_AUTH_REFRESH" ) type ( + // Configuration holds the necessary information for Iris Auth & Single-Sign-On feature. + // + // See the `New` package-level function. Configuration struct { + // The authorization header keys that server should read the access token from. + // + // Defaults to: + // - Authorization + // - X-Authorization + Headers []string `json:"headers" yaml:"Headers" toml:"Headers" ini:"Headers"` + // Cookie optional configuration. + // A Cookie.Name holds the access token value fully encrypted. Cookie CookieConfiguration `json:"cookie" yaml:"Cookie" toml:"Cookie" ini:"cookie"` - // keep it to always renew the refresh token. RefreshStrategy string `json:"refresh_strategy" yaml:"RefreshStrategy" toml:"RefreshStrategy" ini:"refresh_strategy"` + // Keys MUST define the jwt keys configuration for access, + // and optionally, for refresh tokens signing and verification. Keys jwt.KeysConfiguration `json:"keys" yaml:"Keys" toml:"Keys" ini:"keys"` } + // CookieConfiguration holds the necessary information for cookie client storage. CookieConfiguration struct { - Name string `json:"cookie" yaml:"Name" toml:"Name" ini:"name"` - Hash string `json:"hash" yaml:"Hash" toml:"Hash" ini:"hash"` + // Name defines the cookie's name. + Name string `json:"cookie" yaml:"Name" toml:"Name" ini:"name"` + // Hash is optional, it is used to authenticate cookie value using HMAC. + // It is recommended to use a key with 32 or 64 bytes. + Hash string `json:"hash" yaml:"Hash" toml:"Hash" ini:"hash"` + // Block is optional, used to encrypt cookie value. + // The key length must correspond to the block size + // of the encryption algorithm. For AES, used by default, valid lengths are + // 16, 24, or 32 bytes to select AES-128, AES-192, or AES-256. Block string `json:"block" yaml:"Block" toml:"Block" ini:"block"` } ) func (c *Configuration) validate() (jwt.Keys, error) { + if len(c.Headers) == 0 { + return nil, fmt.Errorf("auth: configuration: headers slice is empty") + } + if c.Cookie.Name != "" { if c.Cookie.Hash == "" || c.Cookie.Block == "" { - return nil, fmt.Errorf("cookie block and cookie hash are required for security reasons when cookie is used") + return nil, fmt.Errorf("auth: configuration: cookie block and cookie hash are required for security reasons when cookie is used") } } keys, err := c.Keys.Load() if err != nil { - return nil, fmt.Errorf("sso: %w", err) + return nil, fmt.Errorf("auth: configuration: %w", err) } if _, ok := keys[KIDAccess]; !ok { - return nil, fmt.Errorf("sso: %s access token is missing from the configuration", KIDAccess) + return nil, fmt.Errorf("auth: configuration: %s access token is missing from the configuration", KIDAccess) } // Let's keep refresh optional. // if _, ok := keys[KIDRefresh]; !ok { - // return nil, fmt.Errorf("sso: %s refresh token is missing from the configuration", KIDRefresh) + // return nil, fmt.Errorf("auth: configuration: %s refresh token is missing from the configuration", KIDRefresh) // } return keys, nil } // BindRandom binds the "c" configuration to random values for keys and cookie security. // Keys will not be persisted between restarts, -// a more persistent storage should be considered for production applications. +// a more persistent storage should be considered for production applications, +// see BindFile method and LoadConfiguration/MustLoadConfiguration package-level functions. func (c *Configuration) BindRandom() error { accessPublic, accessPrivate, err := jwt.GenerateEdDSA() if err != nil { @@ -72,8 +99,12 @@ func (c *Configuration) BindRandom() error { } *c = Configuration{ + Headers: []string{ + "Authorization", + "X-Authorization", + }, Cookie: CookieConfiguration{ - Name: "iris_sso", + Name: "iris_auth_cookie", Hash: string(securecookie.GenerateRandomKey(64)), Block: string(securecookie.GenerateRandomKey(32)), }, @@ -99,6 +130,9 @@ func (c *Configuration) BindRandom() error { return nil } +// BindFile binds a filename (fullpath) to "c" Configuration. +// The file format is either JSON or YAML and it should be suffixed +// with .json or .yml/.yaml. func (c *Configuration) BindFile(filename string) error { switch filepath.Ext(filename) { case ".json": @@ -131,14 +165,18 @@ func (c *Configuration) BindFile(filename string) error { } +// ToYAML returns the "c" Configuration's contents as raw yaml byte slice. func (c *Configuration) ToYAML() ([]byte, error) { return yaml.Marshal(c) } +// ToJSON returns the "c" Configuration's contents as raw json byte slice. func (c *Configuration) ToJSON() ([]byte, error) { return json.Marshal(c) } +// MustGenerateConfiguration calls the Configuration's BindRandom +// method and returns the result. It panics on errors. func MustGenerateConfiguration() (c Configuration) { if err := c.BindRandom(); err != nil { panic(err) @@ -147,11 +185,16 @@ func MustGenerateConfiguration() (c Configuration) { return } +// LoadConfiguration reads a filename (fullpath) +// and returns a Configuration binded to the contents of the given filename. +// See Configuration.BindFile method too. func LoadConfiguration(filename string) (c Configuration, err error) { err = c.BindFile(filename) return } +// MustLoadConfiguration same as LoadConfiguration package-level function +// but it panics on errors. func MustLoadConfiguration(filename string) Configuration { c, err := LoadConfiguration(filename) if err != nil { diff --git a/auth/provider.go b/auth/provider.go new file mode 100644 index 00000000..1c1ccf9a --- /dev/null +++ b/auth/provider.go @@ -0,0 +1,86 @@ +//go:build go1.18 + +package auth + +import ( + stdContext "context" + + "github.com/kataras/iris/v12/context" + "github.com/kataras/iris/v12/x/errors" + + "github.com/kataras/jwt" +) + +// VerifiedToken holds the information about a verified token. +type VerifiedToken = jwt.VerifiedToken + +// Provider is an interface of T which MUST be completed +// by a custom value type to provide user information to the Auth's +// JWT Token Signer and Verifier. +// +// A provider can implement Transformer and ErrorHandler and ClaimsProvider as well. +type Provider[T User] interface { + // Signin accepts a username (or email) and a password and should + // return a valid T value or an error describing + // the user authentication or verification reason of failure. + // + // It's called on auth.SigninHandler + Signin(ctx stdContext.Context, username, password string) (T, error) + + // ValidateToken accepts the standard JWT claims and the T value obtained + // by the Signin method and should return a nil error on validation success + // or a non-nil error for validation failure. + // It is mostly used to perform checks of the T value's struct fields or + // the standard claim's (e.g. origin jwt token id). + // It can be an empty method too which returns a nil error. + // + // It's caleld on auth.VerifyHandler. + ValidateToken(ctx stdContext.Context, standardClaims StandardClaims, t T) error + + // InvalidateToken is optional and can be used to allow tokens to be invalidated + // from server-side. Commonly, implement when a token and user pair is saved + // on a persistence storage and server can decide which token is valid or invalid. + // It can be an empty method too which returns a nil error. + // + // It's called on auth.SignoutHandler. + InvalidateToken(ctx stdContext.Context, standardClaims StandardClaims, t T) error + // InvalidateTokens is like InvalidateToken but it should invalidate + // all tokens generated for a specific T value. + // It can be an empty method too which returns a nil error. + // + // It's called on auth.SignoutAllHandler. + InvalidateTokens(ctx stdContext.Context, t T) error +} + +// ClaimsProvider is an optional interface, which may not be used at all. +// If implemented by a Provider, it signs the jwt token +// using these claims to each of the following token types. +type ClaimsProvider interface { + GetAccessTokenClaims() StandardClaims + GetRefreshTokenClaims(accessClaims StandardClaims) StandardClaims +} + +type Transformer[T User] interface { + Transform(ctx stdContext.Context, tok *VerifiedToken) (T, error) +} + +type TransformerFunc[T User] func(ctx stdContext.Context, tok *VerifiedToken) (T, error) + +func (fn TransformerFunc[T]) Transform(ctx stdContext.Context, tok *VerifiedToken) (T, error) { + return fn(ctx, tok) +} + +type ErrorHandler interface { + InvalidArgument(ctx *context.Context, err error) + Unauthenticated(ctx *context.Context, err error) +} + +type DefaultErrorHandler struct{} + +func (e *DefaultErrorHandler) InvalidArgument(ctx *context.Context, err error) { + errors.InvalidArgument.Details(ctx, "unable to parse body", err.Error()) +} + +func (e *DefaultErrorHandler) Unauthenticated(ctx *context.Context, err error) { + errors.Unauthenticated.Err(ctx, err) +} diff --git a/sso/user.go b/auth/user.go similarity index 69% rename from sso/user.go rename to auth/user.go index 5cd0b010..f08b18ab 100644 --- a/sso/user.go +++ b/auth/user.go @@ -1,6 +1,6 @@ //go:build go1.18 -package sso +package auth import ( "github.com/kataras/iris/v12/context" @@ -13,13 +13,13 @@ type ( User = interface{} // any type. ) -const accessTokenContextKey = "iris.sso.context.access_token" +const accessTokenContextKey = "iris.auth.context.access_token" func GetAccessToken(ctx *context.Context) string { return ctx.Values().GetString(accessTokenContextKey) } -const standardClaimsContextKey = "iris.sso.context.standard_claims" +const standardClaimsContextKey = "iris.auth.context.standard_claims" func GetStandardClaims(ctx *context.Context) StandardClaims { if v := ctx.Values().Get(standardClaimsContextKey); v != nil { @@ -31,11 +31,11 @@ func GetStandardClaims(ctx *context.Context) StandardClaims { return StandardClaims{} } -func (s *SSO[T]) GetStandardClaims(ctx *context.Context) StandardClaims { +func (s *Auth[T]) GetStandardClaims(ctx *context.Context) StandardClaims { return GetStandardClaims(ctx) } -const userContextKey = "iris.sso.context.user" +const userContextKey = "iris.auth.context.user" func GetUser[T User](ctx *context.Context) T { if v := ctx.Values().Get(userContextKey); v != nil { @@ -48,6 +48,6 @@ func GetUser[T User](ctx *context.Context) T { return empty } -func (s *SSO[T]) GetUser(ctx *context.Context) T { +func (s *Auth[T]) GetUser(ctx *context.Context) T { return GetUser[T](ctx) } diff --git a/sso/provider.go b/sso/provider.go deleted file mode 100644 index 816c2d79..00000000 --- a/sso/provider.go +++ /dev/null @@ -1,83 +0,0 @@ -//go:build go1.18 - -package sso - -import ( - stdContext "context" - "fmt" - - "github.com/kataras/iris/v12/context" - "github.com/kataras/iris/v12/middleware/jwt" - "github.com/kataras/iris/v12/x/errors" -) - -type VerifiedToken = jwt.VerifiedToken - -type Provider[T User] interface { // A provider can implement Transformer and ErrorHandler as well. - Signin(ctx stdContext.Context, username, password string) (T, error) - - // We could do this instead of transformer below but let's keep separated logic methods: - // ValidateToken(ctx context.Context, tok *VerifiedToken, t *T) error - ValidateToken(ctx stdContext.Context, standardClaims StandardClaims, t T) error - - InvalidateToken(ctx stdContext.Context, standardClaims StandardClaims, t T) error - InvalidateTokens(ctx stdContext.Context, t T) error -} - -// ClaimsProvider is an optional interface, which may not be used at all. -// If completed by a Provider, it signs the jwt token -// using these claims to each of the following token types. -type ClaimsProvider interface { - GetAccessTokenClaims() StandardClaims - GetRefreshTokenClaims(accessClaims StandardClaims) StandardClaims -} - -type Transformer[T User] interface { - Transform(ctx stdContext.Context, tok *VerifiedToken) (T, error) -} - -type TransformerFunc[T User] func(ctx stdContext.Context, tok *VerifiedToken) (T, error) - -func (fn TransformerFunc[T]) Transform(ctx stdContext.Context, tok *VerifiedToken) (T, error) { - return fn(ctx, tok) -} - -type ErrorHandler interface { - InvalidArgument(ctx *context.Context, err error) - Unauthenticated(ctx *context.Context, err error) -} - -type DefaultErrorHandler struct{} - -func (e *DefaultErrorHandler) InvalidArgument(ctx *context.Context, err error) { - errors.InvalidArgument.Details(ctx, "unable to parse body", err.Error()) -} - -func (e *DefaultErrorHandler) Unauthenticated(ctx *context.Context, err error) { - errors.Unauthenticated.Err(ctx, err) -} - -type provider[T User] struct{} - -func newProvider[T User]() *provider[T] { - return new(provider[T]) -} - -func (p *provider[T]) Signin(ctx stdContext.Context, username, password string) (T, error) { // fired on SigninHandler. - // your database... - var t T - return t, fmt.Errorf("user not found") -} - -func (p *provider[T]) ValidateToken(ctx stdContext.Context, standardClaims StandardClaims, t T) error { // fired on VerifyHandler. - // your database and checks of blocked tokens... - return nil -} - -func (p *provider[T]) InvalidateToken(ctx stdContext.Context, standardClaims StandardClaims, t T) error { // fired on SignoutHandler. - return nil -} - -func (p *provider[T]) InvalidateTokens(ctx stdContext.Context, t T) error { // fired on SignoutAllHandler. - return nil -} From 2f9ddff5a9d2670e881645645df56e8bf1b6bd2d Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Sat, 2 Apr 2022 17:41:06 +0300 Subject: [PATCH 06/20] update dependencies --- go.mod | 18 +++++++++--------- go.sum | 38 +++++++++++++++++++------------------- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/go.mod b/go.mod index 30959e4c..147b0402 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/fatih/structs v1.1.0 github.com/flosch/pongo2/v4 v4.0.2 github.com/go-redis/redis/v8 v8.11.5 - github.com/goccy/go-json v0.9.5 + github.com/goccy/go-json v0.9.7-0.20220325155717-3a4ad3198047 github.com/golang/snappy v0.0.4 github.com/google/uuid v1.3.0 github.com/gorilla/securecookie v1.1.1 @@ -36,17 +36,17 @@ require ( github.com/microcosm-cc/bluemonday v1.0.18 github.com/russross/blackfriday/v2 v2.1.0 github.com/schollz/closestmatch v2.1.0+incompatible - github.com/shirou/gopsutil/v3 v3.22.2 + github.com/shirou/gopsutil/v3 v3.22.3 github.com/tdewolff/minify/v2 v2.10.0 github.com/vmihailenco/msgpack/v5 v5.3.5 github.com/yosssi/ace v0.0.5 go.etcd.io/bbolt v1.3.6 - golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd - golang.org/x/net v0.0.0-20220225172249-27dd8689420f - golang.org/x/sys v0.0.0-20220318055525-2edf467146b5 + golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 + golang.org/x/net v0.0.0-20220401154927-543a649e0bdd + golang.org/x/sys v0.0.0-20220330033206-e17cdc41300f golang.org/x/text v0.3.7 golang.org/x/time v0.0.0-20220224211638-0e9765cccd65 - google.golang.org/protobuf v1.27.1 + google.golang.org/protobuf v1.28.0 gopkg.in/ini.v1 v1.66.4 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b ) @@ -92,10 +92,10 @@ require ( github.com/sergi/go-diff v1.2.0 // indirect github.com/sirupsen/logrus v1.8.1 // indirect github.com/smartystreets/assertions v1.2.1 // indirect - github.com/stretchr/testify v1.7.0 // indirect + github.com/stretchr/testify v1.7.1 // indirect github.com/tdewolff/parse/v2 v2.5.27 // indirect - github.com/tklauser/go-sysconf v0.3.9 // indirect - github.com/tklauser/numcpus v0.3.0 // indirect + github.com/tklauser/go-sysconf v0.3.10 // indirect + github.com/tklauser/numcpus v0.4.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect diff --git a/go.sum b/go.sum index d117201f..20039abc 100644 --- a/go.sum +++ b/go.sum @@ -62,8 +62,8 @@ github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.1.0 h1:7RFti/xnNkMJnrK7D1yQ/iCIB5OrrY/54/H930kIbHA= github.com/gobwas/ws v1.1.0/go.mod h1:nzvNcVha5eUziGrbxFCo6qFIojQHjJV5cLYIbezhfL0= -github.com/goccy/go-json v0.9.5 h1:ooSMW526ZjK+EaL5elrSyN2EzIfi/3V0m4+HJEDYLik= -github.com/goccy/go-json v0.9.5/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/goccy/go-json v0.9.7-0.20220325155717-3a4ad3198047 h1:SMQ4NGzEnbUgyY0ids2HuBTOFSUPOjL3GRh5l7zwrvk= +github.com/goccy/go-json v0.9.7-0.20220325155717-3a4ad3198047/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= @@ -179,8 +179,8 @@ github.com/schollz/closestmatch v2.1.0+incompatible h1:Uel2GXEpJqOWBrlyI+oY9LTiy github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g= github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= -github.com/shirou/gopsutil/v3 v3.22.2 h1:wCrArWFkHYIdDxx/FSfF5RB4dpJYW6t7rcp3+zL8uks= -github.com/shirou/gopsutil/v3 v3.22.2/go.mod h1:WapW1AOOPlHyXr+yOyw3uYx36enocrtSoSBy0L5vUHY= +github.com/shirou/gopsutil/v3 v3.22.3 h1:UebRzEomgMpv61e3hgD1tGooqX5trFbdU/ehphbHd00= +github.com/shirou/gopsutil/v3 v3.22.3/go.mod h1:D01hZJ4pVHPpCTZ3m3T2+wDF2YAGfd+H4ifUguaQzHM= github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/smartystreets/assertions v1.2.1 h1:bKNHfEv7tSIjZ8JbKaFjzFINljxG4lzZvmHUnElzOIg= @@ -201,18 +201,19 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/tdewolff/minify/v2 v2.10.0 h1:ovVAHUcjfGrBDf1EIvsodRUVJiZK/28mMose08B7k14= github.com/tdewolff/minify/v2 v2.10.0/go.mod h1:6XAjcHM46pFcRE0eztigFPm0Q+Cxsw8YhEWT+rDkcZM= github.com/tdewolff/parse/v2 v2.5.27 h1:PL3LzzXaOpmdrknnOlIeO2muIBHAwiKp6TxN1RbU5gI= github.com/tdewolff/parse/v2 v2.5.27/go.mod h1:WzaJpRSbwq++EIQHYIRTpbYKNA3gn9it1Ik++q4zyho= github.com/tdewolff/test v1.0.6 h1:76mzYJQ83Op284kMT+63iCNCI7NEERsIN8dLM+RiKr4= github.com/tdewolff/test v1.0.6/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE= -github.com/tklauser/go-sysconf v0.3.9 h1:JeUVdAOWhhxVcU6Eqr/ATFHgXk/mmiItdKeJPev3vTo= -github.com/tklauser/go-sysconf v0.3.9/go.mod h1:11DU/5sG7UexIrp/O6g35hrWzu0JxlwQ3LSFUzyeuhs= -github.com/tklauser/numcpus v0.3.0 h1:ILuRUQBtssgnxw0XXIjKUC56fgnOrFoQQ/4+DeU2biQ= -github.com/tklauser/numcpus v0.3.0/go.mod h1:yFGUr7TUHQRAhyqBcEg0Ge34zDBAsIvJJcyE6boqnA8= +github.com/tklauser/go-sysconf v0.3.10 h1:IJ1AZGZRWbY8T5Vfk04D9WOA5WSejdflXxP03OUqALw= +github.com/tklauser/go-sysconf v0.3.10/go.mod h1:C8XykCvCb+Gn0oNCWPIlcb0RuglQTYaQ2hGm7jmxEFk= +github.com/tklauser/numcpus v0.4.0 h1:E53Dm1HjH1/R2/aoCtXtPgzmElmn51aOkhCFSuZq//o= +github.com/tklauser/numcpus v0.4.0/go.mod h1:1+UI3pD8NW14VMwdgJNJ1ESk2UnwhAnz5hMwiKKqXCQ= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= @@ -243,14 +244,14 @@ go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210314154223-e6e6c4f2bb5b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd h1:XcWmESyNjXJMLahc3mqVQJcgSTDxFxhETVlfk9uGc38= -golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 h1:tkVvjkPTB7pnW3jnid7kNyAMPVWllTNOf/qKDze4p9o= +golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220225172249-27dd8689420f h1:oA4XRj0qtSt8Yo1Zms0CUlsT3KG69V2UGQWPBxujDmc= -golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220401154927-543a649e0bdd h1:zYlwaUHTmxuf6H7hwO2dgwqozQmH7zf4x+/qql4oVWc= +golang.org/x/net v0.0.0-20220401154927-543a649e0bdd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190130150945-aca44879d564/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -264,10 +265,9 @@ golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201207223542-d4d67f95c62d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210816074244-15123e1e1f71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220111092808-5a964db01320/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220318055525-2edf467146b5 h1:saXMvIOKvRFwbOMicHXr0B1uwoxq9dGmLe5ExMES6c4= -golang.org/x/sys v0.0.0-20220318055525-2edf467146b5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220330033206-e17cdc41300f h1:rlezHXNlxYWvBCzNses9Dlc7nGFaNMJeqLolcmQSSZY= +golang.org/x/sys v0.0.0-20220330033206-e17cdc41300f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -283,8 +283,8 @@ golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1N golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= -google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From 872dd45359132154b9813ad645f34628c71bf097 Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Sat, 2 Apr 2022 18:17:47 +0300 Subject: [PATCH 07/20] auth: add an option to enforce the secure attr of the set-cookie --- _examples/auth/auth/auth.yml | 1 + _examples/mvc/websocket-auth/auth.yml | 1 + auth/auth.go | 2 +- auth/configuration.go | 13 ++++++++++--- 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/_examples/auth/auth/auth.yml b/_examples/auth/auth/auth.yml index e324132f..9cfed272 100644 --- a/_examples/auth/auth/auth.yml +++ b/_examples/auth/auth/auth.yml @@ -3,6 +3,7 @@ Headers: # required. - "X-Authorization" Cookie: # optional. Name: "iris_auth_cookie" + Secure: false Hash: "D*G-KaPdSgUkXp2s5v8y/B?E(H+MbQeThWmYq3t6w9z$C&F)J@NcRfUjXn2r4u7x" # length of 64 characters (512-bit). Block: "VkYp3s6v9y$B&E)H@McQfTjWmZq4t7w!" # length of 32 characters (256-bit). Keys: diff --git a/_examples/mvc/websocket-auth/auth.yml b/_examples/mvc/websocket-auth/auth.yml index e324132f..9cfed272 100644 --- a/_examples/mvc/websocket-auth/auth.yml +++ b/_examples/mvc/websocket-auth/auth.yml @@ -3,6 +3,7 @@ Headers: # required. - "X-Authorization" Cookie: # optional. Name: "iris_auth_cookie" + Secure: false Hash: "D*G-KaPdSgUkXp2s5v8y/B?E(H+MbQeThWmYq3t6w9z$C&F)J@NcRfUjXn2r4u7x" # length of 64 characters (512-bit). Block: "VkYp3s6v9y$B&E)H@McQfTjWmZq4t7w!" # length of 32 characters (256-bit). Keys: diff --git a/auth/auth.go b/auth/auth.go index 3e8d405d..de413b5a 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -526,7 +526,7 @@ func (s *Auth[T]) trySetCookie(ctx *context.Context, accessToken string) { Name: cookieName, Value: url.QueryEscape(accessToken), HttpOnly: true, - Secure: ctx.IsSSL(), + Secure: s.config.Cookie.Secure || ctx.IsSSL(), Domain: ctx.Domain(), SameSite: http.SameSiteLaxMode, Expires: time.Now().Add(maxAge), diff --git a/auth/configuration.go b/auth/configuration.go index c5200541..eb81ea47 100644 --- a/auth/configuration.go +++ b/auth/configuration.go @@ -45,6 +45,12 @@ type ( CookieConfiguration struct { // Name defines the cookie's name. Name string `json:"cookie" yaml:"Name" toml:"Name" ini:"name"` + // Secure if true then "; Secure" is appended to the Set-Cookie header. + // By setting the secure to true, the web browser will prevent the + // transmission of a cookie over an unencrypted channel. + // + // Defaults to false but it's true when the request is under iris.Context.IsSSL(). + Secure bool `json:"secure" yaml:"Secure" toml:"Secure" ini:"secure"` // Hash is optional, it is used to authenticate cookie value using HMAC. // It is recommended to use a key with 32 or 64 bytes. Hash string `json:"hash" yaml:"Hash" toml:"Hash" ini:"hash"` @@ -104,9 +110,10 @@ func (c *Configuration) BindRandom() error { "X-Authorization", }, Cookie: CookieConfiguration{ - Name: "iris_auth_cookie", - Hash: string(securecookie.GenerateRandomKey(64)), - Block: string(securecookie.GenerateRandomKey(32)), + Name: "iris_auth_cookie", + Secure: false, + Hash: string(securecookie.GenerateRandomKey(64)), + Block: string(securecookie.GenerateRandomKey(32)), }, Keys: jwt.KeysConfiguration{ { From c5139b22eebe8c4453e4787dce6c8e87eacefe5d Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Sat, 2 Apr 2022 20:45:47 +0300 Subject: [PATCH 08/20] minor --- _examples/cookies/options/main.go | 9 +++++++++ _examples/sessions/basic/main.go | 6 +++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/_examples/cookies/options/main.go b/_examples/cookies/options/main.go index 70d7e272..d45973b2 100644 --- a/_examples/cookies/options/main.go +++ b/_examples/cookies/options/main.go @@ -41,6 +41,15 @@ func withCookieOptions(ctx iris.Context) { // * CookieExpires // * CookieEncoding ctx.AddCookieOptions(iris.CookieAllowReclaim()) + // ctx.AddCookieOptions(iris.CookieSecure) + // OR for a specific cookie: + // ctx.SetCookieKV("cookie_name", "cookie_value", iris.CookieScure) + // OR by passing a a &http.Cookie: + // ctx.SetCookie(&http.Cookie{ + // Name: "cookie_name", + // Value: "cookie_value", + // Secure: true, + // }) ctx.Next() } diff --git a/_examples/sessions/basic/main.go b/_examples/sessions/basic/main.go index 5827ed88..93944c40 100644 --- a/_examples/sessions/basic/main.go +++ b/_examples/sessions/basic/main.go @@ -38,7 +38,11 @@ func logout(ctx iris.Context) { func main() { app := iris.New() - sess := sessions.New(sessions.Config{Cookie: cookieNameForSessionID, AllowReclaim: true}) + sess := sessions.New(sessions.Config{ + Cookie: cookieNameForSessionID, + // CookieSecureTLS: true, + AllowReclaim: true, + }) app.Use(sess.Handler()) // ^ or comment this line and use sess.Start(ctx) inside your handlers // instead of sessions.Get(ctx). From dceb09d4fff13ade5360eb099409e5981e5a36ce Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Sat, 2 Apr 2022 21:14:46 +0300 Subject: [PATCH 09/20] auth: godoc: provider, claims provider, transformer, error handler and user helpers --- auth/auth.go | 13 +++++++++++++ auth/provider.go | 36 +++++++++++++++++++++++++++++++----- auth/user.go | 17 ++++++++++++----- 3 files changed, 56 insertions(+), 10 deletions(-) diff --git a/auth/auth.go b/auth/auth.go index de413b5a..3d1537bf 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -90,6 +90,19 @@ func New[T User](config Configuration) (*Auth[T], error) { return s, nil } +// WithProviderAndErrorHandler registers a provider (if not nil) and +// an error handler (if not nil) and returns this "s" Auth instance. +// It's the same as calling AddProvider and SetErrorHandler at once. +// It's really useful when registering an Auth instance using the iris.Party.PartyConfigure +// method when a Provider of T and ErrorHandler is available through the registered Party's dependencies. +// +// Usage Example: +// api := app.Party("/api") +// api.EnsureStaticBindings().RegisterDependency( +// NewAuthProviderErrorHandler(), +// NewAuthCustomerProvider, +// auth.Must(auth.New[Customer](authConfig)).WithProviderAndErrorHandler, +// ) func (s *Auth[T]) WithProviderAndErrorHandler(provider Provider[T], errHandler ErrorHandler) *Auth[T] { if provider != nil { for i := range s.providers { diff --git a/auth/provider.go b/auth/provider.go index 1c1ccf9a..5e2171bb 100644 --- a/auth/provider.go +++ b/auth/provider.go @@ -18,13 +18,15 @@ type VerifiedToken = jwt.VerifiedToken // by a custom value type to provide user information to the Auth's // JWT Token Signer and Verifier. // -// A provider can implement Transformer and ErrorHandler and ClaimsProvider as well. +// A provider can optionally complete the Transformer, ClaimsProvider and +// ErrorHandler all in once when necessary. +// Set a provider using the AddProvider method of Auth type. type Provider[T User] interface { // Signin accepts a username (or email) and a password and should // return a valid T value or an error describing // the user authentication or verification reason of failure. // - // It's called on auth.SigninHandler + // It's called on Auth.SigninHandler Signin(ctx stdContext.Context, username, password string) (T, error) // ValidateToken accepts the standard JWT claims and the T value obtained @@ -34,7 +36,7 @@ type Provider[T User] interface { // the standard claim's (e.g. origin jwt token id). // It can be an empty method too which returns a nil error. // - // It's caleld on auth.VerifyHandler. + // It's caleld on Auth.VerifyHandler. ValidateToken(ctx stdContext.Context, standardClaims StandardClaims, t T) error // InvalidateToken is optional and can be used to allow tokens to be invalidated @@ -42,13 +44,13 @@ type Provider[T User] interface { // on a persistence storage and server can decide which token is valid or invalid. // It can be an empty method too which returns a nil error. // - // It's called on auth.SignoutHandler. + // It's called on Auth.SignoutHandler. InvalidateToken(ctx stdContext.Context, standardClaims StandardClaims, t T) error // InvalidateTokens is like InvalidateToken but it should invalidate // all tokens generated for a specific T value. // It can be an empty method too which returns a nil error. // - // It's called on auth.SignoutAllHandler. + // It's called on Auth.SignoutAllHandler. InvalidateTokens(ctx stdContext.Context, t T) error } @@ -60,27 +62,51 @@ type ClaimsProvider interface { GetRefreshTokenClaims(accessClaims StandardClaims) StandardClaims } +// Transformer is an optional interface which can be implemented by a Provider as well. +// Set a Transformer through Auth.SetTransformer or Auth.SetTransformerFunc or by implementing +// the Transform method inside a Provider which can be registered through the Auth.AddProvider +// method. +// +// A transformer is called on Auth.VerifyHandler before Provider.ValidateToken and it can +// be used to modify the T value based on the token's contents. It is mostly used +// to convert the json claims to T value manually, when they differ. type Transformer[T User] interface { Transform(ctx stdContext.Context, tok *VerifiedToken) (T, error) } +// TransformerFunc completes the Transformer interface. type TransformerFunc[T User] func(ctx stdContext.Context, tok *VerifiedToken) (T, error) +// Transform calls itself. func (fn TransformerFunc[T]) Transform(ctx stdContext.Context, tok *VerifiedToken) (T, error) { return fn(ctx, tok) } +// ErrorHandler is an optional interface which can be implemented by a Provider as well. +// +// ErrorHandler is the interface which controls the HTTP errors on +// Auth.SigninHandler, Auth.VerifyHandler, Auth.SignoutHandler and +// Auth.SignoutAllHandler handelrs. type ErrorHandler interface { + // InvalidArgument should handle any 400 (bad request) errors, + // e.g. invalid request body. InvalidArgument(ctx *context.Context, err error) + // Unauthenticated should handle any 401 (unauthenticated) errors, + // e.g. user not found or invalid credentials. Unauthenticated(ctx *context.Context, err error) } +// DefaultErrorHandler is the default registered ErrorHandler which can be +// replaced through the Auth.SetErrorHandler method. type DefaultErrorHandler struct{} +// InvalidArgument sends 400 (bad request) with "unable to parse body" as its message +// and the "err" value as its details. func (e *DefaultErrorHandler) InvalidArgument(ctx *context.Context, err error) { errors.InvalidArgument.Details(ctx, "unable to parse body", err.Error()) } +// Unauthenticated sends 401 (unauthenticated) with the "err" value as its message. func (e *DefaultErrorHandler) Unauthenticated(ctx *context.Context, err error) { errors.Unauthenticated.Err(ctx, err) } diff --git a/auth/user.go b/auth/user.go index f08b18ab..885afd20 100644 --- a/auth/user.go +++ b/auth/user.go @@ -9,18 +9,25 @@ import ( ) type ( + // StandardClaims is an alias of jwt.Claims, it holds the standard JWT claims. StandardClaims = jwt.Claims - User = interface{} // any type. + // User is an alias of an empty interface, it's here to declare the typeof T, + // which can be any custom struct type. + User = interface{} ) const accessTokenContextKey = "iris.auth.context.access_token" +// GetAccessToken accepts the iris Context and returns the raw access token value. +// It's only available after Auth.VerifyHandler is executed. func GetAccessToken(ctx *context.Context) string { return ctx.Values().GetString(accessTokenContextKey) } const standardClaimsContextKey = "iris.auth.context.standard_claims" +// GetStandardClaims accepts the iris Context and returns the standard token's claims. +// It's only available after Auth.VerifyHandler is executed. func GetStandardClaims(ctx *context.Context) StandardClaims { if v := ctx.Values().Get(standardClaimsContextKey); v != nil { if c, ok := v.(StandardClaims); ok { @@ -31,12 +38,10 @@ func GetStandardClaims(ctx *context.Context) StandardClaims { return StandardClaims{} } -func (s *Auth[T]) GetStandardClaims(ctx *context.Context) StandardClaims { - return GetStandardClaims(ctx) -} - const userContextKey = "iris.auth.context.user" +// GetUser is the package-level function of the Auth.GetUser method. +// It returns the T user value after Auth.VerifyHandler is executed. func GetUser[T User](ctx *context.Context) T { if v := ctx.Values().Get(userContextKey); v != nil { if t, ok := v.(T); ok { @@ -48,6 +53,8 @@ func GetUser[T User](ctx *context.Context) T { return empty } +// GetUser accepts the iris Context and returns the T custom user/claims struct value. +// It's only available after Auth.VerifyHandler is executed. func (s *Auth[T]) GetUser(ctx *context.Context) T { return GetUser[T](ctx) } From f17a325df678ad1b85ff97bcddd1121605d543ce Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Thu, 7 Apr 2022 01:56:42 +0300 Subject: [PATCH 10/20] minor improvements --- _examples/auth/auth/main.go | 2 +- auth/auth.go | 121 ++++++++++++++++++++++++++---------- auth/configuration.go | 18 +++--- auth/provider.go | 19 +++++- auth/user.go | 4 +- context/context.go | 7 ++- 6 files changed, 122 insertions(+), 49 deletions(-) diff --git a/_examples/auth/auth/main.go b/_examples/auth/auth/main.go index 110948e7..c05a4133 100644 --- a/_examples/auth/auth/main.go +++ b/_examples/auth/auth/main.go @@ -9,7 +9,7 @@ import ( "github.com/kataras/iris/v12/auth" ) -func allowRole(role AccessRole) auth.TVerify[User] { +func allowRole(role AccessRole) auth.VerifyUserFunc[User] { return func(u User) error { if !u.Role.Allow(role) { return fmt.Errorf("invalid role") diff --git a/auth/auth.go b/auth/auth.go index 3d1537bf..b1c752f7 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -18,51 +18,87 @@ import ( ) type ( + // Auth holds the necessary functionality to authorize and optionally authenticating + // users to access and perform actions against the resource server (Iris API). + // It completes a secure and fast JSON Web Token signer and verifier which, + // based on the custom application needs, can be further customized. + // Each Auth of T instance can sign and verify a single custom instance, + // more Auth instances can share the same configuration to support multiple custom user types. + // Initialize a new Auth of T instance using the New or MustLoad package-level functions. + // Most important methods of the instance are: + // - AddProvider + // - SigninHandler + // - VerifyHandler + // - SignoutHandler + // - SignoutAllHandler + // + // Example can be found at: https://github.com/kataras/iris/tree/master/_examples/auth/auth/main.go. Auth[T User] struct { + // Holds the configuration passed through the New and MustLoad + // package-level functions. One or more Auth instance can share the + // same configuration's values. config Configuration - - keys jwt.Keys + // Holds the result of the config.KeysConfiguration. + keys jwt.Keys + // This is an Iris cookie option used to encrypt and decrypt a cookie when + // the config.Cookie.Hash & Block are not empty. securecookie context.SecureCookie - - providers []Provider[T] // at least one. - errorHandler ErrorHandler - transformer Transformer[T] + // Defaults to an empty list, which cannot sign any tokens. + // One or more custom providers should be registered through + // the AddProvider or WithProviderAndErrorHandler methods. + providers []Provider[T] // at least one. + // Always not nil, set to custom error handler on SetErrorHandler. + errorHandler ErrorHandler + // Not nil if a transformer is registered. + transformer Transformer[T] + // Not nil if a custom claims provider is registered. claimsProvider ClaimsProvider - refreshEnabled bool // if KIDRefresh exists in keys. + // True if KIDRefresh on config.Keys. + refreshEnabled bool } - TVerify[T User] func(t T) error + // VerifyUserFunc is passed on Verify and VerifyHandler method + // to, optionally, further validate a T user value. + VerifyUserFunc[T User] func(t T) error + // SigninRequest is the request body the server expects + // on SignHandler. The Password and Username or Email should be filled. SigninRequest struct { Username string `json:"username" form:"username,omitempty"` // username OR email, username has priority over email. Email string `json:"email" form:"email,omitempty"` // email OR username. Password string `json:"password" form:"password"` } + // SigninResponse is the response body the server sends + // to the client on the SignHandler. It contains a pair of the access token + // and the refresh token if the refresh jwt token id exists in the configuration. SigninResponse struct { AccessToken string `json:"access_token"` RefreshToken string `json:"refresh_token,omitempty"` } + // RefreshRequest is the request body the server expects + // on VerifyHandler to renew an access and refresh token pair. RefreshRequest struct { RefreshToken string `json:"refresh_token"` } ) +// MustLoad binds a filename (fullpath) configuration yaml or json +// and constructs a new Auth instance. It panics on error. func MustLoad[T User](filename string) *Auth[T] { var config Configuration if err := config.BindFile(filename); err != nil { panic(err) } - s, err := New[T](config) - if err != nil { - panic(err) - } - - return s + return Must(New[T](config)) } +// Must is a helper that wraps a call to a function returning (*Auth[T], error) +// and panics if the error is non-nil. It is intended for use in variable +// initializations such as +// var s = auth.Must(auth.New[MyUser](config)) func Must[T User](s *Auth[T], err error) *Auth[T] { if err != nil { panic(err) @@ -71,6 +107,14 @@ func Must[T User](s *Auth[T], err error) *Auth[T] { return s } +// New initializes a new Auth instance typeof T and returns it. +// The T generic can be any custom struct. +// It accepts a Configuration value which can be constructed +// manually or through a configuration file using the +// MustGenerateConfiguration or MustLoadConfiguration +// or LoadConfiguration or MustLoad package-level functions. +// +// Example can be found at: https://github.com/kataras/iris/tree/master/_examples/auth/auth/main.go. func New[T User](config Configuration) (*Auth[T], error) { keys, err := config.validate() if err != nil { @@ -121,6 +165,8 @@ func (s *Auth[T]) WithProviderAndErrorHandler(provider Provider[T], errHandler E return s } +// AddProvider registers one or more providers to this Auth of T instance and returns itself. +// Look the Provider godoc for more. func (s *Auth[T]) AddProvider(providers ...Provider[T]) *Auth[T] { // A provider can also implement both transformer and // error handler if that's the design option of the end-developer. @@ -146,21 +192,31 @@ func (s *Auth[T]) AddProvider(providers ...Provider[T]) *Auth[T] { return s } +// SetErrorHandler sets a custom error handler to this Auth of T instance and returns itself. +// Look the Provider and ErrorHandler godoc for more. func (s *Auth[T]) SetErrorHandler(errHandler ErrorHandler) *Auth[T] { s.errorHandler = errHandler return s } +// SetTransformer sets a custom transformer to this Auth of T instance and returns itself. +// Look the Provider and Transformer godoc for more. func (s *Auth[T]) SetTransformer(transformer Transformer[T]) *Auth[T] { s.transformer = transformer return s } +// SetTransformerFunc like SetTransformer method but accepts a function instead. func (s *Auth[T]) SetTransformerFunc(transfermerFunc func(ctx stdContext.Context, tok *VerifiedToken) (T, error)) *Auth[T] { s.transformer = TransformerFunc[T](transfermerFunc) return s } +// Signin signs a token based on the provided username and password +// and returns a pair of access and refresh tokens. +// +// Signin calls the Provider.Signin method to check if a user +// is authenticated by the given username and password combination. func (s *Auth[T]) Signin(ctx stdContext.Context, username, password string) ([]byte, []byte, error) { var t T @@ -292,12 +348,22 @@ func (s *Auth[T]) SigninHandler(ctx *context.Context) { ctx.JSON(resp) } -func (s *Auth[T]) Verify(ctx stdContext.Context, token []byte) (T, StandardClaims, error) { +func (s *Auth[T]) Verify(ctx stdContext.Context, token []byte, verifyFuncs ...VerifyUserFunc[T]) (T, StandardClaims, error) { t, claims, err := s.verify(ctx, token) if err != nil { return t, StandardClaims{}, fmt.Errorf("auth: verify: %w", err) } + for _, verify := range verifyFuncs { + if verify == nil { + continue + } + + if err = verify(t); err != nil { + return t, StandardClaims{}, fmt.Errorf("auth: verify: %w", err) + } + } + return t, claims, nil } @@ -348,7 +414,7 @@ func (s *Auth[T]) verify(ctx stdContext.Context, token []byte) (T, StandardClaim return t, standardClaims, nil } -func (s *Auth[T]) VerifyHandler(verifyFuncs ...TVerify[T]) context.Handler { +func (s *Auth[T]) VerifyHandler(verifyFuncs ...VerifyUserFunc[T]) context.Handler { return func(ctx *context.Context) { accessToken := s.extractAccessToken(ctx) @@ -357,31 +423,18 @@ func (s *Auth[T]) VerifyHandler(verifyFuncs ...TVerify[T]) context.Handler { return } - t, claims, err := s.Verify(ctx, []byte(accessToken)) + t, claims, err := s.Verify(ctx, []byte(accessToken), verifyFuncs...) if err != nil { s.errorHandler.Unauthenticated(ctx, err) return } - for _, verify := range verifyFuncs { - if verify == nil { - continue - } - - if err = verify(t); err != nil { - err = fmt.Errorf("auth: verify: %v", err) - s.errorHandler.Unauthenticated(ctx, err) - return - } - } - ctx.SetUser(t) // store the user to the request. ctx.Values().Set(accessTokenContextKey, accessToken) - - ctx.Values().Set(userContextKey, t) ctx.Values().Set(standardClaimsContextKey, claims) + ctx.Values().Set(userContextKey, t) ctx.Next() } @@ -504,8 +557,8 @@ func (s *Auth[T]) signoutHandler(ctx *context.Context, all bool) { ctx.SetUser(nil) ctx.Values().Remove(accessTokenContextKey) - ctx.Values().Remove(userContextKey) ctx.Values().Remove(standardClaimsContextKey) + ctx.Values().Remove(userContextKey) } func (s *Auth[T]) extractTokenFromHeader(ctx *context.Context) string { @@ -535,9 +588,9 @@ func (s *Auth[T]) trySetCookie(ctx *context.Context, accessToken string) { } cookie := &http.Cookie{ - Path: "/", Name: cookieName, Value: url.QueryEscape(accessToken), + Path: "/", HttpOnly: true, Secure: s.config.Cookie.Secure || ctx.IsSSL(), Domain: ctx.Domain(), @@ -546,7 +599,7 @@ func (s *Auth[T]) trySetCookie(ctx *context.Context, accessToken string) { MaxAge: int(maxAge.Seconds()), } - ctx.SetCookie(cookie, context.CookieEncoding(s.securecookie)) + ctx.SetCookie(cookie, context.CookieEncoding(s.securecookie), context.CookieAllowReclaim()) } } diff --git a/auth/configuration.go b/auth/configuration.go index eb81ea47..fb3ea57e 100644 --- a/auth/configuration.go +++ b/auth/configuration.go @@ -192,16 +192,8 @@ func MustGenerateConfiguration() (c Configuration) { return } -// LoadConfiguration reads a filename (fullpath) -// and returns a Configuration binded to the contents of the given filename. -// See Configuration.BindFile method too. -func LoadConfiguration(filename string) (c Configuration, err error) { - err = c.BindFile(filename) - return -} - // MustLoadConfiguration same as LoadConfiguration package-level function -// but it panics on errors. +// but it panics on error. func MustLoadConfiguration(filename string) Configuration { c, err := LoadConfiguration(filename) if err != nil { @@ -210,3 +202,11 @@ func MustLoadConfiguration(filename string) Configuration { return c } + +// LoadConfiguration reads a filename (fullpath) +// and returns a Configuration binded to the contents of the given filename. +// See Configuration.BindFile method too. +func LoadConfiguration(filename string) (c Configuration, err error) { + err = c.BindFile(filename) + return +} diff --git a/auth/provider.go b/auth/provider.go index 5e2171bb..9261e60f 100644 --- a/auth/provider.go +++ b/auth/provider.go @@ -21,12 +21,17 @@ type VerifiedToken = jwt.VerifiedToken // A provider can optionally complete the Transformer, ClaimsProvider and // ErrorHandler all in once when necessary. // Set a provider using the AddProvider method of Auth type. +// +// Example can be found at: https://github.com/kataras/iris/tree/master/_examples/auth/auth/user_provider.go. type Provider[T User] interface { // Signin accepts a username (or email) and a password and should // return a valid T value or an error describing // the user authentication or verification reason of failure. // - // It's called on Auth.SigninHandler + // The first input argument standard context can be + // casted to iris.Context if executed through Auth.SigninHandler. + // + // It's called on Auth.SigninHandler. Signin(ctx stdContext.Context, username, password string) (T, error) // ValidateToken accepts the standard JWT claims and the T value obtained @@ -36,6 +41,9 @@ type Provider[T User] interface { // the standard claim's (e.g. origin jwt token id). // It can be an empty method too which returns a nil error. // + // The first input argument standard context can be + // casted to iris.Context if executed through Auth.VerifyHandler. + // // It's caleld on Auth.VerifyHandler. ValidateToken(ctx stdContext.Context, standardClaims StandardClaims, t T) error @@ -44,12 +52,18 @@ type Provider[T User] interface { // on a persistence storage and server can decide which token is valid or invalid. // It can be an empty method too which returns a nil error. // + // The first input argument standard context can be + // casted to iris.Context if executed through Auth.SignoutHandler. + // // It's called on Auth.SignoutHandler. InvalidateToken(ctx stdContext.Context, standardClaims StandardClaims, t T) error // InvalidateTokens is like InvalidateToken but it should invalidate // all tokens generated for a specific T value. // It can be an empty method too which returns a nil error. // + // The first input argument standard context can be + // casted to iris.Context if executed through Auth.SignoutAllHandler. + // // It's called on Auth.SignoutAllHandler. InvalidateTokens(ctx stdContext.Context, t T) error } @@ -70,6 +84,9 @@ type ClaimsProvider interface { // A transformer is called on Auth.VerifyHandler before Provider.ValidateToken and it can // be used to modify the T value based on the token's contents. It is mostly used // to convert the json claims to T value manually, when they differ. +// +// The first input argument standard context can be +// casted to iris.Context if executed through Auth.VerifyHandler. type Transformer[T User] interface { Transform(ctx stdContext.Context, tok *VerifiedToken) (T, error) } diff --git a/auth/user.go b/auth/user.go index 885afd20..9958077d 100644 --- a/auth/user.go +++ b/auth/user.go @@ -13,7 +13,9 @@ type ( StandardClaims = jwt.Claims // User is an alias of an empty interface, it's here to declare the typeof T, // which can be any custom struct type. - User = interface{} + // + // Example can be found at: https://github.com/kataras/iris/tree/master/_examples/auth/auth/user.go. + User = any ) const accessTokenContextKey = "iris.auth.context.access_token" diff --git a/context/context.go b/context/context.go index 030f41b8..01e63c2c 100644 --- a/context/context.go +++ b/context/context.go @@ -3865,9 +3865,10 @@ func WriteJSON(writer io.Writer, v interface{}, options JSON, shouldOptimize boo err error ) - if !shouldOptimize && options.Indent == "" { - options.Indent = " " - } + // Let's keep it as it is. + // if !shouldOptimize && options.Indent == "" { + // options.Indent = " " + // } if indent := options.Indent; indent != "" { if shouldOptimize { From 4dfd4c53d36ba3bebc5f12f431d8676f7b986640 Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Sat, 9 Apr 2022 14:51:34 +0300 Subject: [PATCH 11/20] new minor features --- HISTORY.md | 4 + _examples/project/api/configuration.go | 4 +- _examples/project/api/router.go | 19 +- _examples/project/go.mod | 19 +- _examples/project/go.sum | 47 ++-- _examples/project/pkg/database/database.go | 9 + _examples/project/server.yml | 1 + _examples/project/user/repository.go | 8 +- aliases.go | 7 + context/context.go | 26 +++ middleware/methodoverride/methodoverride.go | 4 + middleware/modrevision/modrevision.go | 102 ++++++++ x/pagination/pagination.go | 246 ++++++++++++++++++++ x/reflex/{reflectx.go => reflex.go} | 0 14 files changed, 455 insertions(+), 41 deletions(-) create mode 100644 _examples/project/pkg/database/database.go create mode 100644 middleware/modrevision/modrevision.go create mode 100644 x/pagination/pagination.go rename x/reflex/{reflectx.go => reflex.go} (100%) diff --git a/HISTORY.md b/HISTORY.md index 4b08299a..64c6df8f 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -28,6 +28,10 @@ The codebase for Dependency Injection, Internationalization and localization and ## Fixes and Improvements +- Add new [x/pagination](x/pagination/pagination.go) sub-package which supports generics code (go 1.18+). +- Add new [middleware/modrevision](middleware/modrevision) middleware (example at [_examples/project/api/router.go]_examples/project/api/router.go). +- Add `iris.BuildRevision` and `iris.BuildTime` to embrace the new go's 1.18 debug build information. + - Add `Context.SetJSONOptions` to customize on a higher level the JSON options on `Context.JSON` calls. - Add new [auth](auth) sub-package which helps on any user type auth using JWT (access & refresh tokens) and a cookie (optional). diff --git a/_examples/project/api/configuration.go b/_examples/project/api/configuration.go index d7cc2cc7..3acff670 100644 --- a/_examples/project/api/configuration.go +++ b/_examples/project/api/configuration.go @@ -12,6 +12,7 @@ import ( // for our server, including the Iris one. type Configuration struct { ServerName string `yaml:"ServerName"` + Env string `yaml:"Env"` // The server's host, if empty, defaults to 0.0.0.0 Host string `yaml:"Host"` // The server's port, e.g. 80 @@ -27,7 +28,8 @@ type Configuration struct { // If not empty a request logger is registered, // note that this will cost a lot in performance, use it only for debug. RequestLog string `yaml:"RequestLog"` - + // The database connection string. + ConnString string `yaml:"ConnString"` // Iris specific configuration. Iris iris.Configuration `yaml:"Iris"` } diff --git a/_examples/project/api/router.go b/_examples/project/api/router.go index abd681be..c5668b7f 100644 --- a/_examples/project/api/router.go +++ b/_examples/project/api/router.go @@ -1,22 +1,31 @@ package api import ( + "time" + "github.com/username/project/api/users" + "github.com/username/project/pkg/database" "github.com/username/project/user" - "github.com/kataras/iris/v12" + "github.com/kataras/iris/v12/middleware/modrevision" ) // buildRouter is the most important part of your server. // All root endpoints are registered here. func (srv *Server) buildRouter() { // Add a simple health route. - srv.Any("/health", func(ctx iris.Context) { - ctx.Writef("%s\n\nOK", srv.String()) - }) + srv.Any("/health", modrevision.New(modrevision.Options{ + ServerName: srv.config.ServerName, + Env: srv.config.Env, + Developer: "kataras", + TimeLocation: time.FixedZone("Greece/Athens", 10800), + })) api := srv.Party("/api") - api.RegisterDependency(user.NewRepository) + api.RegisterDependency( + database.Open(srv.config.ConnString), + user.NewRepository, + ) api.PartyConfigure("/user", new(users.API)) } diff --git a/_examples/project/go.mod b/_examples/project/go.mod index 60fa8b9d..9219b965 100644 --- a/_examples/project/go.mod +++ b/_examples/project/go.mod @@ -2,6 +2,8 @@ module github.com/username/project go 1.17 +replace github.com/kataras/iris/v12 => ../../ + require ( github.com/kataras/golog v0.1.7 github.com/kataras/iris/v12 v12.2.0-alpha6.0.20220224214946-37c766fef748 @@ -16,11 +18,10 @@ require ( github.com/Shopify/goreferrer v0.0.0-20210630161223-536fa16abd6f // indirect github.com/andybalholm/brotli v1.0.4 // indirect github.com/aymerick/douceur v0.2.0 // indirect - github.com/aymerick/raymond v2.0.2+incompatible // indirect github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385 // indirect github.com/fatih/structs v1.1.0 // indirect github.com/flosch/pongo2/v4 v4.0.2 // indirect - github.com/goccy/go-json v0.9.4 // indirect + github.com/goccy/go-json v0.9.7-0.20220325155717-3a4ad3198047 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/google/uuid v1.3.0 // indirect github.com/gorilla/css v1.0.0 // indirect @@ -33,13 +34,15 @@ require ( github.com/kataras/pio v0.0.10 // indirect github.com/kataras/sitemap v0.0.5 // indirect github.com/kataras/tunnel v0.0.3 // indirect - github.com/klauspost/compress v1.14.3 // indirect + github.com/klauspost/compress v1.15.1 // indirect + github.com/mailgun/raymond/v2 v2.0.46 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/microcosm-cc/bluemonday v1.0.18 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/schollz/closestmatch v2.1.0+incompatible // indirect + github.com/sirupsen/logrus v1.8.1 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/tdewolff/minify/v2 v2.10.0 // indirect github.com/tdewolff/parse/v2 v2.5.27 // indirect @@ -47,11 +50,11 @@ require ( github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/yosssi/ace v0.0.5 // indirect - golang.org/x/crypto v0.0.0-20220214200702-86341886e292 // indirect - golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd // indirect - golang.org/x/sys v0.0.0-20220209214540-3681064d5158 // indirect + golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 // indirect + golang.org/x/net v0.0.0-20220401154927-543a649e0bdd // indirect + golang.org/x/sys v0.0.0-20220330033206-e17cdc41300f // indirect golang.org/x/text v0.3.7 // indirect - golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect - google.golang.org/protobuf v1.27.1 // indirect + golang.org/x/time v0.0.0-20220224211638-0e9765cccd65 // indirect + google.golang.org/protobuf v1.28.0 // indirect gopkg.in/ini.v1 v1.66.4 // indirect ) diff --git a/_examples/project/go.sum b/_examples/project/go.sum index 6194b69a..4d893ed4 100644 --- a/_examples/project/go.sum +++ b/_examples/project/go.sum @@ -74,8 +74,6 @@ github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= -github.com/aymerick/raymond v2.0.2+incompatible h1:VEp3GpgdAnv9B2GFyTvqgcKvY+mfKMjPOA3SbKLtnU0= -github.com/aymerick/raymond v2.0.2+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= @@ -140,8 +138,8 @@ github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2 github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/goccy/go-json v0.9.4 h1:L8MLKG2mvVXiQu07qB6hmfqeSYQdOnqPot2GhsIwIaI= -github.com/goccy/go-json v0.9.4/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/goccy/go-json v0.9.7-0.20220325155717-3a4ad3198047 h1:SMQ4NGzEnbUgyY0ids2HuBTOFSUPOjL3GRh5l7zwrvk= +github.com/goccy/go-json v0.9.7-0.20220325155717-3a4ad3198047/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= @@ -194,7 +192,7 @@ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= @@ -224,7 +222,7 @@ github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pf github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= -github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/hashicorp/consul/api v1.11.0/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M= github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms= @@ -262,7 +260,7 @@ github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1: github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/iris-contrib/httpexpect/v2 v2.0.5 h1:b2Orx2FXRhnmZil4td66C8zzkHnssSoFQP2HQtyktJg= +github.com/iris-contrib/httpexpect/v2 v2.3.1 h1:A69ilxKGW1jDRKK5UAhjTL4uJYh3RjD4qzt9vNZ7fpY= github.com/iris-contrib/jade v1.1.4 h1:WoYdfyJFfZIUgqNAeOyRfTNQZOksSlZ6+FnXR3AEpX0= github.com/iris-contrib/jade v1.1.4/go.mod h1:EDqR+ur9piDl6DUgs6qRrlfzmlx/D5UybogqrXvJTBE= github.com/iris-contrib/schema v0.0.6 h1:CPSBLyx2e91H2yJzPuhGuifVRnZBBJ3pCOMbOvPZaTw= @@ -281,8 +279,6 @@ github.com/kataras/blocks v0.0.5 h1:jFrsHEDfXZhHTbhkNWgMgpfEQNj1Bwr1IYEYZ9Xxoxg= github.com/kataras/blocks v0.0.5/go.mod h1:kcJIuvuA8QmGKFLHIZHdCAPCjcE85IhttzXd6W+ayfE= github.com/kataras/golog v0.1.7 h1:0TY5tHn5L5DlRIikepcaRR/6oInIr9AiWsxzt0vvlBE= github.com/kataras/golog v0.1.7/go.mod h1:jOSQ+C5fUqsNSwurB/oAHq1IFSb0KI3l6GMa7xB6dZA= -github.com/kataras/iris/v12 v12.2.0-alpha6.0.20220224214946-37c766fef748 h1:8zXAxFQUMY11OkYq2qzSJRwnEpJVBjJfg2yswDESJrk= -github.com/kataras/iris/v12 v12.2.0-alpha6.0.20220224214946-37c766fef748/go.mod h1:41s7glJCO96To+fzPzTYn1ttaxlcBnYvp0iOccd0oGE= github.com/kataras/pio v0.0.10 h1:b0qtPUqOpM2O+bqa5wr2O6dN4cQNwSmFd6HQqgVae0g= github.com/kataras/pio v0.0.10/go.mod h1:gS3ui9xSD+lAUpbYnjOGiQyY7sUMJO+EHpiRzhtZ5no= github.com/kataras/sitemap v0.0.5 h1:4HCONX5RLgVy6G4RkYOV3vKNcma9p236LdGOipJsaFE= @@ -291,8 +287,8 @@ github.com/kataras/tunnel v0.0.3 h1:+8eHXujPD3wLnqTbYtPGa/3/Jc+Eq+bsPwEGTeFBB00= github.com/kataras/tunnel v0.0.3/go.mod h1:VOlCoaUE5zN1buE+yAjWCkjfQ9hxGuhomKLsjei/5Zs= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.14.3 h1:DQv1WP+iS4srNjibdnHtqu8JNWCDMluj5NzPnFJsnvk= -github.com/klauspost/compress v1.14.3/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/klauspost/compress v1.15.1 h1:y9FcTHGyrebwfP0ZZqFiaxTaiDnUrGkJkI+f583BL1A= +github.com/klauspost/compress v1.15.1/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= @@ -303,6 +299,8 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/lyft/protoc-gen-star v0.5.3/go.mod h1:V0xaHgaf5oCCqmcxYcWiDfTiKsZsRc87/1qhoTACD8w= github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= +github.com/mailgun/raymond/v2 v2.0.46 h1:aOYHhvTpF5USySJ0o7cpPno/Uh2I5qg2115K25A+Ft4= +github.com/mailgun/raymond/v2 v2.0.46/go.mod h1:lsgvL50kgt1ylcFJYZiULi5fjPBkkhNfj4KA0W54Z18= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/matryer/try v0.0.0-20161228173917-9ac251b645a2/go.mod h1:0KeJpeMD6o+O4hW7qJOT7vyQPKrWmj26uf5wMc/IiIs= @@ -363,7 +361,6 @@ github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsT github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= @@ -371,10 +368,11 @@ github.com/sagikazarmark/crypt v0.3.0/go.mod h1:uD/D+6UF4SrIR1uGEv7bBNkNqLGqUr43 github.com/schollz/closestmatch v2.1.0+incompatible h1:Uel2GXEpJqOWBrlyI+oY9LTiyyjYS17cCYRqP13/SHk= github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= -github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= -github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4= github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= @@ -392,8 +390,8 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/tdewolff/minify/v2 v2.10.0 h1:ovVAHUcjfGrBDf1EIvsodRUVJiZK/28mMose08B7k14= github.com/tdewolff/minify/v2 v2.10.0/go.mod h1:6XAjcHM46pFcRE0eztigFPm0Q+Cxsw8YhEWT+rDkcZM= @@ -445,8 +443,8 @@ golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20220214200702-86341886e292 h1:f+lwQ+GtmgoY+A2YaQxlSOnDjXcQ7ZRLWOHbC6HtRqE= -golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 h1:tkVvjkPTB7pnW3jnid7kNyAMPVWllTNOf/qKDze4p9o= +golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -525,8 +523,8 @@ golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd h1:O7DYs+zxREGLKzKoMQrtrEacpb0ZVXA5rIwylE2Xchk= -golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220401154927-543a649e0bdd h1:zYlwaUHTmxuf6H7hwO2dgwqozQmH7zf4x+/qql4oVWc= +golang.org/x/net v0.0.0-20220401154927-543a649e0bdd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -620,8 +618,8 @@ golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220209214540-3681064d5158 h1:rm+CHSpPEEW2IsXUib1ThaHIjuBVZjxNgSKmBLFfD4c= -golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220330033206-e17cdc41300f h1:rlezHXNlxYWvBCzNses9Dlc7nGFaNMJeqLolcmQSSZY= +golang.org/x/sys v0.0.0-20220330033206-e17cdc41300f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -636,8 +634,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= -golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20220224211638-0e9765cccd65 h1:M73Iuj3xbbb9Uk1DYhzydthsj6oOd6l9bpuFcNoUvTs= +golang.org/x/time v0.0.0-20220224211638-0e9765cccd65/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= @@ -839,8 +837,9 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/_examples/project/pkg/database/database.go b/_examples/project/pkg/database/database.go new file mode 100644 index 00000000..80e5687c --- /dev/null +++ b/_examples/project/pkg/database/database.go @@ -0,0 +1,9 @@ +package database + +type DB struct { + /* ... */ +} + +func Open(connString string) *DB { + return &DB{} +} diff --git a/_examples/project/server.yml b/_examples/project/server.yml index 1edcd1a8..0784028a 100644 --- a/_examples/project/server.yml +++ b/_examples/project/server.yml @@ -1,4 +1,5 @@ ServerName: My Project +Env: development Host: 0.0.0.0 Port: 80 EnableCompression: true diff --git a/_examples/project/user/repository.go b/_examples/project/user/repository.go index 4ec98547..99c67a12 100644 --- a/_examples/project/user/repository.go +++ b/_examples/project/user/repository.go @@ -1,12 +1,14 @@ package user +import "github.com/username/project/pkg/database" + type Repository interface { // Repo methods here... } type repo struct { // Hold database instance here: e.g. - // *mydatabase_pkg.DB + db *database.DB } -func NewRepository( /* *mydatabase_pkg.DB */ ) Repository { - return &repo{ /* db: db */ } +func NewRepository(db *database.DB) Repository { + return &repo{db: db} } diff --git a/aliases.go b/aliases.go index 6c18ebcb..89416580 100644 --- a/aliases.go +++ b/aliases.go @@ -14,6 +14,13 @@ import ( "github.com/kataras/iris/v12/view" ) +var ( + // BuildRevision holds the vcs commit id information. + BuildRevision = context.BuildRevision + // BuildTime holds the vcs commit time information. + BuildTime = context.BuildTime +) + // SameSite attributes. const ( SameSiteDefaultMode = http.SameSiteDefaultMode diff --git a/context/context.go b/context/context.go index 01e63c2c..8b726012 100644 --- a/context/context.go +++ b/context/context.go @@ -19,6 +19,7 @@ import ( "path/filepath" "reflect" "regexp" + "runtime/debug" "sort" "strconv" "strings" @@ -45,6 +46,31 @@ import ( "gopkg.in/yaml.v3" ) +var ( + // BuildRevision holds the vcs commit id information. + BuildRevision string + // BuildTime holds the vcs commit time information. + BuildTime string +) + +func init() { + if info, ok := debug.ReadBuildInfo(); ok { + for _, setting := range info.Settings { + if BuildRevision != "" && BuildTime != "" { + break + } + + if setting.Key == "vcs.revision" { + BuildRevision = setting.Value + } + + if setting.Key == "vcs.time" { + BuildTime = setting.Key + } + } + } +} + type ( // BodyDecoder is an interface which any struct can implement in order to customize the decode action // from ReadJSON and ReadXML diff --git a/middleware/methodoverride/methodoverride.go b/middleware/methodoverride/methodoverride.go index d6c2fbce..032b6833 100644 --- a/middleware/methodoverride/methodoverride.go +++ b/middleware/methodoverride/methodoverride.go @@ -9,6 +9,10 @@ import ( "github.com/kataras/iris/v12/core/router" ) +func init() { + context.SetHandlerName("iris/middleware/methodoverride.*", "iris.methodoverride") +} + type options struct { getters []GetterFunc methods []string diff --git a/middleware/modrevision/modrevision.go b/middleware/modrevision/modrevision.go new file mode 100644 index 00000000..ca40b620 --- /dev/null +++ b/middleware/modrevision/modrevision.go @@ -0,0 +1,102 @@ +package modrevision + +import ( + "fmt" + "strings" + "time" + + "github.com/kataras/iris/v12/context" +) + +func init() { + context.SetHandlerName("iris/middleware/modrevision.*", "iris.modrevision") +} + +// Options holds the necessary values to render the server name, environment and build information. +// See the `New` package-level function. +type Options struct { + // The ServerName, e.g. Iris Server. + ServerName string + // The Environment, e.g. development. + Env string + // The Developer, e.g. kataras. + Developer string + // True to display the build time as unix (seconds). + UnixTime bool + // A non nil time location value to customize the display of the build time. + TimeLocation *time.Location +} + +type modRevision struct { + options Options + + buildTime string + buildRevision string + + contents []byte +} + +// New returns an Iris Handler which renders +// the server name (env), build information (if available) +// and an OK message. The handler displays simple debug information such as build commit id and time. +// It does NOT render information about the Go language itself or any operating system confgiuration +// for security reasons. +// +// Example Code: +// app.Get("/health", modrevision.New(modrevision.Options{ +// ServerName: "Iris Server", +// Env: "development", +// Developer: "kataras", +// TimeLocation: time.FixedZone("Greece/Athens", 10800), +// })) +func New(opts Options) context.Handler { + bTime, bRevision := context.BuildTime, context.BuildRevision + if opts.UnixTime { + if t, err := time.Parse(time.RFC3339, bTime); err == nil { + bTime = fmt.Sprintf("%d", t.Unix()) + } + } else if opts.TimeLocation != nil { + if t, err := time.Parse(time.RFC3339, bTime); err == nil { + bTime = t.In(opts.TimeLocation).String() + } + } + + m := &modRevision{ + options: opts, + + buildTime: bTime, + buildRevision: bRevision, + } + + contents := []byte(m.String()) + if len(contents) > 0 { + contents = append(contents, []byte("\n\nOK")...) + } else { + contents = []byte("OK") + } + + return func(ctx *context.Context) { + ctx.Write(contents) + } +} + +// String returns the server name and its running environment or an empty string +// of the given server name is empty. +func (m *modRevision) String() string { + if name := m.options.ServerName; name != "" { + if env := m.options.Env; env != "" { + name += fmt.Sprintf(" (%s)", env) + } + + if m.buildRevision != "" && m.buildTime != "" { + buildTitle := ">>>> build" // if we ever want an emoji, there is one: \U0001f4bb + tab := strings.Repeat(" ", len(buildTitle)) + name += fmt.Sprintf("\n\n%[1]s\n%srevision %s\n[1]sbuildtime %s\n[1]sdeveloper %s", tab, + buildTitle, m.buildRevision, m.buildTime, m.options.Developer) + } + + return name + } + + return "" +} diff --git a/x/pagination/pagination.go b/x/pagination/pagination.go new file mode 100644 index 00000000..8becf6b9 --- /dev/null +++ b/x/pagination/pagination.go @@ -0,0 +1,246 @@ +//go:build go1.18 + +/* +Until go version 2, we can't really apply the type alias feature on a generic type or function, +so keep it separated on x/pagination. + +import "github.com/kataras/iris/v12/context" + +type ListResponse[T any] = context.ListResponse[T] +OR +type ListResponse = context.ListResponse doesn't work. + +The only workable thing for generic aliases is when you know the type e.g. +type ListResponse = context.ListResponse[any] but that doesn't fit us. +*/ + +package iris + +import ( + "math" + "net/http" + "strconv" +) + +var ( + // MaxSize defines the max size of items to display. + MaxSize = 100000 + // DefaultSize defines the default size when ListOptions.Size is zero. + DefaultSize = MaxSize +) + +// ListOptions is the list request object which should be provided by the client through +// URL Query. Then the server passes that options to a database query, +// including any custom filters may be given from the request body and, +// then the server responds back with a `Context.JSON(NewList(...))` response based +// on the database query's results. +type ListOptions struct { + // Current page number. + // If Page > 0 then: + // Limit = DefaultLimit + // Offset = DefaultLimit * Page + // If Page == 0 then no actual data is return, + // internally we must check for this value + // because in postgres LIMIT 0 returns the columns but with an empty set. + Page int `json:"page" url:"page"` + // The elements to get, this modifies the LIMIT clause, + // this Size can't be higher than the MaxSize. + // If Size is zero then size is set to DefaultSize. + Size int `json:"size" url:"size"` +} + +// GetLimit returns the LIMIT value of a query. +func (opts ListOptions) GetLimit() int { + if opts.Size > 0 && opts.Size < MaxSize { + return opts.Size + } + + return DefaultSize +} + +// GetLimit returns the OFFSET value of a query. +func (opts ListOptions) GetOffset() int { + if opts.Page > 1 { + return (opts.Page - 1) * opts.GetLimit() + } + + return 0 +} + +// GetCurrentPage returns the Page or 1. +func (opts ListOptions) GetCurrentPage() int { + current := opts.Page + if current == 0 { + current = 1 + } + + return current +} + +// GetNextPage returns the next page, current page + 1. +func (opts ListOptions) GetNextPage() int { + return opts.GetCurrentPage() + 1 +} + +// Bind binds the ListOptions values to a request value. +// It should be used as an x/client.RequestOption to fire requests +// on a server that supports pagination. +func (opts ListOptions) Bind(r *http.Request) error { + page := strconv.Itoa(opts.GetCurrentPage()) + size := strconv.Itoa(opts.GetLimit()) + + q := r.URL.Query() + q.Set("page", page) + q.Set("size", size) + return nil +} + +// List is the http response of a server handler which should render +// items with pagination support. +type List[T any] struct { + CurrentPage int `json:"current_page"` // the current page. + PageSize int `json:"page_size"` // the total amount of the entities return. + TotalPages int `json:"total_pages"` // the total number of pages based on page, size and total count. + TotalItems int64 `json:"total_items"` // the total number of rows. + HasNextPage bool `json:"has_next_page"` // true if more data can be fetched, depending on the current page * page size and total pages. + Filter any `json:"filter"` // if any filter data. + Items []T `json:"items"` // Items is empty array if no objects returned. Do NOT modify from outside. +} + +// NewList returns a new List response which holds +// the current page, page size, total pages, total items count, any custom filter +// and the items array. +// +// Example Code: +// +// import "github.com/kataras/iris/v12/x/pagination" +// ...more code +// +// type User struct { +// Firstname string `json:"firstname"` +// Lastname string `json:"lastname"` +// } +// +// type ExtraUser struct { +// User +// ExtraData string +// } +// +// func main() { +// users := []User{ +// {"Gerasimos", "Maropoulos"}, +// {"Efi", "Kwfidou"}, +// } +// +// t := pagination.NewList(users, 100, nil, pagination.ListOptions{ +// Page: 1, +// Size: 50, +// }) +// +// // Optionally, transform a T list of objects to a V list of objects. +// v, err := pagination.TransformList(t, func(u User) (ExtraUser, error) { +// return ExtraUser{ +// User: u, +// ExtraData: "test extra data", +// }, nil +// }) +// if err != nil { panic(err) } +// +// paginationJSON, err := json.MarshalIndent(v, "", " ") +// if err!=nil { panic(err) } +// fmt.Println(paginationJSON) +// } +func NewList[T any](items []T, totalCount int64, filter any, opts ListOptions) *List[T] { + pageSize := opts.GetLimit() + + n := len(items) + if n == 0 || pageSize <= 0 { + return &List[T]{ + CurrentPage: 1, + PageSize: 0, + TotalItems: 0, + TotalPages: 0, + Filter: filter, + Items: make([]T, 0), + } + } + + numberOfPages := int(roundUp(float64(totalCount)/float64(pageSize), 0)) + if numberOfPages <= 0 { + numberOfPages = 1 + } + + var hasNextPage bool + + currentPage := opts.GetCurrentPage() + if totalCount == 0 { + currentPage = 1 + } + + if n > 0 { + hasNextPage = currentPage < numberOfPages + } + + return &List[T]{ + CurrentPage: currentPage, + PageSize: n, + TotalPages: numberOfPages, + TotalItems: totalCount, + HasNextPage: hasNextPage, + Filter: filter, + Items: items, + } +} + +// TransformList accepts a List response and converts to a list of V items. +// T => from +// V => to +// +// Example Code: +// +// listOfUsers := pagination.NewList(...) +// newListOfExtraUsers, err := pagination.TransformList(listOfUsers, func(u User) (ExtraUser, error) { +// return ExtraUser{ +// User: u, +// ExtraData: "test extra data", +// }, nil +// }) +func TransformList[T any, V any](list *List[T], transform func(T) (V, error)) (*List[V], error) { + if list == nil { + return &List[V]{ + CurrentPage: 1, + PageSize: 0, + TotalItems: 0, + TotalPages: 0, + Filter: nil, + Items: make([]V, 0), + }, nil + } + + items := list.Items + + toItems := make([]V, 0, len(items)) + for _, fromItem := range items { + toItem, err := transform(fromItem) + if err != nil { + return nil, err + } + + toItems = append(toItems, toItem) + } + + newList := &List[V]{ + CurrentPage: list.CurrentPage, + PageSize: list.PageSize, + TotalItems: list.TotalItems, + TotalPages: list.TotalPages, + Filter: list.Filter, + Items: toItems, + } + return newList, nil +} + +func roundUp(input float64, places float64) float64 { + pow := math.Pow(10, places) + return math.Ceil(pow*input) / pow +} diff --git a/x/reflex/reflectx.go b/x/reflex/reflex.go similarity index 100% rename from x/reflex/reflectx.go rename to x/reflex/reflex.go From 60bf26eab81b55f0dcece40ac393d911c276b46e Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Sun, 10 Apr 2022 00:19:04 +0300 Subject: [PATCH 12/20] complete the godoc for auth.go --- auth/auth.go | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/auth/auth.go b/auth/auth.go index b1c752f7..93c74d09 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -304,6 +304,9 @@ func (s *Auth[T]) sign(t T) ([]byte, []byte, error) { return accessToken, refreshToken, nil } +// SignHandler generates and sends a pair of access and refresh token to the client +// as JSON body of `SigninResponse` and cookie (if cookie setting was provided). +// See `Signin` method for more. func (s *Auth[T]) SigninHandler(ctx *context.Context) { // No, let the developer decide it based on a middleware, e.g. iris.LimitRequestBodySize. // ctx.SetMaxRequestBodySize(s.maxRequestBodySize) @@ -348,6 +351,8 @@ func (s *Auth[T]) SigninHandler(ctx *context.Context) { ctx.JSON(resp) } +// Verify accepts a token and verifies it. +// It returns the token's custom and standard JWT claims. func (s *Auth[T]) Verify(ctx stdContext.Context, token []byte, verifyFuncs ...VerifyUserFunc[T]) (T, StandardClaims, error) { t, claims, err := s.verify(ctx, token) if err != nil { @@ -414,6 +419,12 @@ func (s *Auth[T]) verify(ctx stdContext.Context, token []byte) (T, StandardClaim return t, standardClaims, nil } +// VerifyHandler verifies and sets the necessary information about the user(claims) and +// the verified token to the Iris Context and calls the Context's Next method. +// This information is available through auth.GetAccessToken, auth.GetStandardClaims and +// auth.GetUser[T] package-level functions. +// +// See `Verify` method for more. func (s *Auth[T]) VerifyHandler(verifyFuncs ...VerifyUserFunc[T]) context.Handler { return func(ctx *context.Context) { accessToken := s.extractAccessToken(ctx) @@ -454,6 +465,8 @@ func (s *Auth[T]) extractAccessToken(ctx *context.Context) string { return accessToken } +// Refresh accepts a previously generated refresh token (from SigninHandler) and +// returns a new access and refresh token pair. func (s *Auth[T]) Refresh(ctx stdContext.Context, refreshToken []byte) ([]byte, []byte, error) { if !s.refreshEnabled { return nil, nil, fmt.Errorf("auth: refresh: disabled") @@ -474,6 +487,10 @@ func (s *Auth[T]) Refresh(ctx stdContext.Context, refreshToken []byte) ([]byte, return accessTok, refreshTok, nil } +// RefreshHandler reads the request body which should include data for `RefreshRequest` structure +// and sends a new access and refresh token pair, +// also sets the cookie to the new encrypted access token value. +// See `Refresh` method for more. func (s *Auth[T]) RefreshHandler(ctx *context.Context) { var req RefreshRequest err := ctx.ReadJSON(&req) @@ -501,6 +518,10 @@ func (s *Auth[T]) RefreshHandler(ctx *context.Context) { ctx.JSON(resp) } +// Signout accepts the access token and a boolean which reports whether +// the signout should be applied to all tokens generated for a specific user (logout from all devices) +// or just the provided token's one. +// It calls the Provider's InvalidateToken(all=false) or InvalidateTokens (all=true). func (s *Auth[T]) Signout(ctx stdContext.Context, token []byte, all bool) error { t, standardClaims, err := s.verify(ctx, token) if err != nil { @@ -531,10 +552,17 @@ func (s *Auth[T]) Signout(ctx stdContext.Context, token []byte, all bool) error return nil } +// SignoutHandler verifies the request's access token and invalidates it, calling +// the Provider's InvalidateToken method. +// See `Signout` method too. func (s *Auth[T]) SignoutHandler(ctx *context.Context) { s.signoutHandler(ctx, false) } +// SignoutAllHandler verifies the request's access token and +// should invalidate all the tokens generated previously calling +// the Provider's InvalidateTokens method. +// See `Signout` method too. func (s *Auth[T]) SignoutAllHandler(ctx *context.Context) { s.signoutHandler(ctx, true) } From b0ccd579afd9228c47238904267c6f216e40491d Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Sun, 10 Apr 2022 01:08:11 +0300 Subject: [PATCH 13/20] add a new _proposals folder and a silly idea --- .deepsource.toml | 2 +- _proposals/generic_handler.md | 81 +++++++++++++++++++++++++++++++++++ context/context.go | 16 +++++-- 3 files changed, 94 insertions(+), 5 deletions(-) create mode 100644 _proposals/generic_handler.md diff --git a/.deepsource.toml b/.deepsource.toml index 17377110..b2c8a5ba 100644 --- a/.deepsource.toml +++ b/.deepsource.toml @@ -13,4 +13,4 @@ name = "go" enabled = true [analyzers.meta] - import_paths = ["github.com/kataras/iris"] + import_paths = ["github.com/kataras/iris/v12"] diff --git a/_proposals/generic_handler.md b/_proposals/generic_handler.md new file mode 100644 index 00000000..950b3643 --- /dev/null +++ b/_proposals/generic_handler.md @@ -0,0 +1,81 @@ +# Iris Handler with Generics support + +```go +package x + +import ( + "github.com/kataras/iris/v12/context" + "github.com/kataras/iris/v12/x/errors" +) + +var ErrorHandler context.ErrorHandler = context.ErrorHandlerFunc(errors.InvalidArgument.Err) + +type ( + Handler[Request any | *context.Context, Response any] func(Request) (Response, error) + HandlerWithCtx[Request any, Response any] func(*context.Context, Request) (Response, error) +) + +func HandleContext[Request any, Response any](handler HandlerWithCtx[Request, Response]) context.Handler { + return func(ctx *context.Context) { + var req Request + if err := ctx.ReadJSON(&req); err != nil { + errors.InvalidArgument.Details(ctx, "unable to parse body", err.Error()) + return + } + + resp, err := handler(ctx, req) + if err != nil { + ErrorHandler.HandleContextError(ctx, err) + return + } + + if _, err = ctx.JSON(resp); err != nil { + errors.Internal.Details(ctx, "unable to parse response", err.Error()) + return + } + } +} + +func Handle[Request any, Response any](handler Handler[Request, Response]) context.Handler { + return HandleContext(func(_ *context.Context, req Request) (Response, error) { return handler(req) }) +} + +``` + +Usage Code: + +```go +import ( + "github.com/kataras/iris/v12" + "github.com/kataras/iris/v12/x" +) + +type ( + Req struct { + Email string `json:"email"` + } + + Res struct { + Verified bool `json:"verified"` + } +) + +func main() { + app := iris.New() + app.Post("/", your_package.Handle(handler)) + app.Listen(":8080") +} + +func handler(req Req) (Res, error){ + verified := req.Email == "iris-go@outlook.com" + return Res{Verified: verified}, nil +} +``` + +Example response: + +```json +{ + "verified": true +} +``` diff --git a/context/context.go b/context/context.go index 8b726012..91120e58 100644 --- a/context/context.go +++ b/context/context.go @@ -3793,10 +3793,18 @@ type JSON struct { ErrorHandler ErrorHandler } -// ErrorHandler describes a context error handler. As for today this is only used -// to globally or per-party or per-route handle JSON writes error. -type ErrorHandler interface { - HandleContextError(ctx *Context, err error) +type ( + // ErrorHandler describes a context error handler. As for today this is only used + // to globally or per-party or per-route handle JSON writes error. + ErrorHandler interface { + HandleContextError(ctx *Context, err error) + } + // ErrorHandlerFunc a function shortcut for ErrorHandler interface. + ErrorHandlerFunc func(ctx *Context, err error) +) + +func (h ErrorHandlerFunc) HandleContextError(ctx *Context, err error) { + h(ctx, err) } // IsDefault reports whether this JSON options structure holds the default values. From 193de0042662035d13c66d472a8005195ee39060 Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Sun, 10 Apr 2022 01:11:11 +0300 Subject: [PATCH 14/20] minor --- aliases.go | 2 ++ context/context.go | 21 ++------------------- context/context_go118.go | 23 +++++++++++++++++++++++ 3 files changed, 27 insertions(+), 19 deletions(-) create mode 100644 context/context_go118.go diff --git a/aliases.go b/aliases.go index 89416580..554c9660 100644 --- a/aliases.go +++ b/aliases.go @@ -16,8 +16,10 @@ import ( var ( // BuildRevision holds the vcs commit id information. + // Available at go version 1.18+ BuildRevision = context.BuildRevision // BuildTime holds the vcs commit time information. + // Available at go version 1.18+ BuildTime = context.BuildTime ) diff --git a/context/context.go b/context/context.go index 91120e58..ef7d99f9 100644 --- a/context/context.go +++ b/context/context.go @@ -19,7 +19,6 @@ import ( "path/filepath" "reflect" "regexp" - "runtime/debug" "sort" "strconv" "strings" @@ -48,29 +47,13 @@ import ( var ( // BuildRevision holds the vcs commit id information. + // Available at go version 1.18+ BuildRevision string // BuildTime holds the vcs commit time information. + // Available at go version 1.18+ BuildTime string ) -func init() { - if info, ok := debug.ReadBuildInfo(); ok { - for _, setting := range info.Settings { - if BuildRevision != "" && BuildTime != "" { - break - } - - if setting.Key == "vcs.revision" { - BuildRevision = setting.Value - } - - if setting.Key == "vcs.time" { - BuildTime = setting.Key - } - } - } -} - type ( // BodyDecoder is an interface which any struct can implement in order to customize the decode action // from ReadJSON and ReadXML diff --git a/context/context_go118.go b/context/context_go118.go new file mode 100644 index 00000000..b1773520 --- /dev/null +++ b/context/context_go118.go @@ -0,0 +1,23 @@ +//go:build go1.18 + +package context + +import "runtime/debug" + +func init() { + if info, ok := debug.ReadBuildInfo(); ok { + for _, setting := range info.Settings { + if BuildRevision != "" && BuildTime != "" { + break + } + + if setting.Key == "vcs.revision" { + BuildRevision = setting.Value + } + + if setting.Key == "vcs.time" { + BuildTime = setting.Key + } + } + } +} From 73dfabf4121c0af69b441dc94a95c44d2626a67b Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Sun, 10 Apr 2022 01:25:19 +0300 Subject: [PATCH 15/20] modrevision:minor --- _examples/project/cmd/cmd.go | 7 ++-- _examples/project/main.go | 7 +--- aliases.go | 5 ++- context/context.go | 4 +- middleware/modrevision/modrevision.go | 59 ++++++++------------------- 5 files changed, 28 insertions(+), 54 deletions(-) diff --git a/_examples/project/cmd/cmd.go b/_examples/project/cmd/cmd.go index 20f3d441..ba4e29f6 100644 --- a/_examples/project/cmd/cmd.go +++ b/_examples/project/cmd/cmd.go @@ -3,6 +3,7 @@ package cmd import ( "github.com/username/project/api" + "github.com/kataras/iris/v12" "github.com/spf13/cobra" ) @@ -13,7 +14,7 @@ var serverConfig api.Configuration // New returns a new CLI app. // Build with: // $ go build -ldflags="-s -w" -func New(buildRevision, buildTime string) *cobra.Command { +func New() *cobra.Command { configFile := defaultConfigFilename rootCmd := &cobra.Command{ @@ -35,8 +36,8 @@ func New(buildRevision, buildTime string) *cobra.Command { } helpTemplate := HelpTemplate{ - BuildRevision: buildRevision, - BuildTime: buildTime, + BuildRevision: iris.BuildRevision, + BuildTime: iris.BuildTime, ShowGoRuntimeVersion: true, } rootCmd.SetHelpTemplate(helpTemplate.String()) diff --git a/_examples/project/main.go b/_examples/project/main.go index 89d00f59..0e8ce255 100644 --- a/_examples/project/main.go +++ b/_examples/project/main.go @@ -7,13 +7,8 @@ import ( "github.com/username/project/cmd" ) -var ( - buildRevision string - buildTime string -) - func main() { - app := cmd.New(buildRevision, buildTime) + app := cmd.New() if err := app.Execute(); err != nil { fmt.Println(err) os.Exit(1) diff --git a/aliases.go b/aliases.go index 554c9660..76968a66 100644 --- a/aliases.go +++ b/aliases.go @@ -15,10 +15,11 @@ import ( ) var ( - // BuildRevision holds the vcs commit id information. + // BuildRevision holds the vcs commit id information of the program's build. + // To display the Iris' version please use the iris.Version constant instead. // Available at go version 1.18+ BuildRevision = context.BuildRevision - // BuildTime holds the vcs commit time information. + // BuildTime holds the vcs commit time information of the program's build. // Available at go version 1.18+ BuildTime = context.BuildTime ) diff --git a/context/context.go b/context/context.go index ef7d99f9..257d0bbe 100644 --- a/context/context.go +++ b/context/context.go @@ -46,10 +46,10 @@ import ( ) var ( - // BuildRevision holds the vcs commit id information. + // BuildRevision holds the vcs commit id information of the program's build. // Available at go version 1.18+ BuildRevision string - // BuildTime holds the vcs commit time information. + // BuildTime holds the vcs commit time information of the program's build. // Available at go version 1.18+ BuildTime string ) diff --git a/middleware/modrevision/modrevision.go b/middleware/modrevision/modrevision.go index ca40b620..198339c8 100644 --- a/middleware/modrevision/modrevision.go +++ b/middleware/modrevision/modrevision.go @@ -27,15 +27,6 @@ type Options struct { TimeLocation *time.Location } -type modRevision struct { - options Options - - buildTime string - buildRevision string - - contents []byte -} - // New returns an Iris Handler which renders // the server name (env), build information (if available) // and an OK message. The handler displays simple debug information such as build commit id and time. @@ -50,25 +41,32 @@ type modRevision struct { // TimeLocation: time.FixedZone("Greece/Athens", 10800), // })) func New(opts Options) context.Handler { - bTime, bRevision := context.BuildTime, context.BuildRevision + buildTime, buildRevision := context.BuildTime, context.BuildRevision if opts.UnixTime { - if t, err := time.Parse(time.RFC3339, bTime); err == nil { - bTime = fmt.Sprintf("%d", t.Unix()) + if t, err := time.Parse(time.RFC3339, buildTime); err == nil { + buildTime = fmt.Sprintf("%d", t.Unix()) } } else if opts.TimeLocation != nil { - if t, err := time.Parse(time.RFC3339, bTime); err == nil { - bTime = t.In(opts.TimeLocation).String() + if t, err := time.Parse(time.RFC3339, buildTime); err == nil { + buildTime = t.In(opts.TimeLocation).String() } } - m := &modRevision{ - options: opts, - - buildTime: bTime, - buildRevision: bRevision, + var buildInfo string + if buildInfo = opts.ServerName; buildInfo != "" { + if env := opts.Env; env != "" { + buildInfo += fmt.Sprintf(" (%s)", env) + } } - contents := []byte(m.String()) + if buildRevision != "" && buildTime != "" { + buildTitle := ">>>> build" + tab := strings.Repeat(" ", len(buildTitle)) + buildInfo += fmt.Sprintf("\n\n%s\n%[2]srevision %[3]s\n%[2]sbuildtime %[4]s\n%[2]sdeveloper %[5]s", + buildTitle, tab, buildRevision, buildTime, opts.Developer) + } + + contents := []byte(buildInfo) if len(contents) > 0 { contents = append(contents, []byte("\n\nOK")...) } else { @@ -79,24 +77,3 @@ func New(opts Options) context.Handler { ctx.Write(contents) } } - -// String returns the server name and its running environment or an empty string -// of the given server name is empty. -func (m *modRevision) String() string { - if name := m.options.ServerName; name != "" { - if env := m.options.Env; env != "" { - name += fmt.Sprintf(" (%s)", env) - } - - if m.buildRevision != "" && m.buildTime != "" { - buildTitle := ">>>> build" // if we ever want an emoji, there is one: \U0001f4bb - tab := strings.Repeat(" ", len(buildTitle)) - name += fmt.Sprintf("\n\n%[1]s\n%srevision %s\n[1]sbuildtime %s\n[1]sdeveloper %s", tab, - buildTitle, m.buildRevision, m.buildTime, m.options.Developer) - } - - return name - } - - return "" -} From ae828d8db9112598f9568108124c0a86f2f91a66 Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Wed, 13 Apr 2022 01:00:53 +0300 Subject: [PATCH 16/20] new Application.SetContextErrorHandler method --- .fossa.yml | 7 +- HISTORY.md | 5 +- configuration.go | 117 ++++++++---- context/application.go | 4 + context/configuration.go | 5 + context/context.go | 395 +++++++++++++++++++-------------------- iris.go | 24 +++ 7 files changed, 314 insertions(+), 243 deletions(-) diff --git a/.fossa.yml b/.fossa.yml index cb3f48ee..f87fafda 100644 --- a/.fossa.yml +++ b/.fossa.yml @@ -1,8 +1,9 @@ -version: 2 +version: 3 cli: server: https://app.fossa.com - fetcher: custom - project: https://github.com/kataras/iris.git + fetcher: git + package: github.com/kataras/iris + project: github.com/kataras/iris analyze: modules: - name: iris diff --git a/HISTORY.md b/HISTORY.md index 64c6df8f..d3afa52c 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -28,11 +28,12 @@ The codebase for Dependency Injection, Internationalization and localization and ## Fixes and Improvements +- Add new `Application.SetContextErrorHandler` to globally customize the default behavior (status code 500 without body) on `JSON`, `JSONP`, `Protobuf`, `MsgPack`, `XML`, `YAML` and `Markdown` method call write errors instead of catching the error on each handler. - Add new [x/pagination](x/pagination/pagination.go) sub-package which supports generics code (go 1.18+). - Add new [middleware/modrevision](middleware/modrevision) middleware (example at [_examples/project/api/router.go]_examples/project/api/router.go). - Add `iris.BuildRevision` and `iris.BuildTime` to embrace the new go's 1.18 debug build information. -- Add `Context.SetJSONOptions` to customize on a higher level the JSON options on `Context.JSON` calls. +- ~Add `Context.SetJSONOptions` to customize on a higher level the JSON options on `Context.JSON` calls.~ update: remains as it's, per JSON call. - Add new [auth](auth) sub-package which helps on any user type auth using JWT (access & refresh tokens) and a cookie (optional). - Add `Party.EnsureStaticBindings` which, if called, the MVC binder panics if a struct's input binding depends on the HTTP request data instead of a static dependency. This is useful to make sure your API crafted through `Party.PartyConfigure` depends only on struct values you already defined at `Party.RegisterDependency` == will never use reflection at serve-time (maximum performance). @@ -71,7 +72,7 @@ The codebase for Dependency Injection, Internationalization and localization and - New `apps.OnApplicationRegistered` method which listens on new Iris applications hosted under the same binary. Use it on your `init` functions to configure Iris applications by any spot in your project's files. -- `Context.JSON` respects any object implements the `easyjson.Marshaler` interface and renders the result using the [easyjon](https://github.com/mailru/easyjson)'s writer. +- `Context.JSON` respects any object implements the `easyjson.Marshaler` interface and renders the result using the [easyjon](https://github.com/mailru/easyjson)'s writer. **Set** the `Configuration.EnableProtoJSON` and `Configuration.EnableEasyJSON` to true in order to enable this feature. - minor: `Context` structure implements the standard go Context interface now (includes: Deadline, Done, Err and Value methods). Handlers can now just pass the `ctx iris.Context` as a shortcut of `ctx.Request().Context()` when needed. diff --git a/configuration.go b/configuration.go index 6d5db5d2..73905652 100644 --- a/configuration.go +++ b/configuration.go @@ -10,11 +10,11 @@ import ( "strings" "time" - "github.com/kataras/golog" "github.com/kataras/iris/v12/context" "github.com/kataras/iris/v12/core/netutil" "github.com/BurntSushi/toml" + "github.com/kataras/golog" "github.com/kataras/sitemap" "github.com/kataras/tunnel" "gopkg.in/yaml.v3" @@ -317,6 +317,20 @@ var WithOptimizations = func(app *Application) { app.config.EnableOptimizations = true } +// WithProtoJSON enables the proto marshaler on Context.JSON method. +// +// See `Configuration` for more. +var WithProtoJSON = func(app *Application) { + app.config.EnableProtoJSON = true +} + +// WithEasyJSON enables the fast easy json marshaler on Context.JSON method. +// +// See `Configuration` for more. +var WithEasyJSON = func(app *Application) { + app.config.EnableEasyJSON = true +} + // WithFireMethodNotAllowed enables the FireMethodNotAllowed setting. // // See `Configuration`. @@ -740,6 +754,17 @@ type Configuration struct { // // Defaults to false. EnableOptimizations bool `ini:"enable_optimizations" json:"enableOptimizations,omitempty" yaml:"EnableOptimizations" toml:"EnableOptimizations"` + // EnableProtoJSON when this field is true + // enables the proto marshaler on given proto messages when calling the Context.JSON method. + // + // Defaults to false. + EnableProtoJSON bool `ini:"enable_proto_json" json:"enableProtoJSON,omitempty" yaml:"EnableProtoJSON" toml:"EnableProtoJSON"` + // EnableEasyJSON when this field is true + // enables the fast easy json marshaler on compatible struct values when calling the Context.JSON method. + // + // Defaults to false. + EnableEasyJSON bool `ini:"enable_easy_json" json:"enableEasyJSON,omitempty" yaml:"EnableEasyJSON" toml:"EnableEasyJSON"` + // DisableBodyConsumptionOnUnmarshal manages the reading behavior of the context's body readers/binders. // If set to true then it // disables the body consumption by the `context.UnmarshalBody/ReadJSON/ReadXML`. @@ -915,177 +940,187 @@ type Configuration struct { var _ context.ConfigurationReadOnly = (*Configuration)(nil) // GetVHost returns the non-exported vhost config field. -func (c Configuration) GetVHost() string { +func (c *Configuration) GetVHost() string { return c.vhost } // GetLogLevel returns the LogLevel field. -func (c Configuration) GetLogLevel() string { +func (c *Configuration) GetLogLevel() string { return c.vhost } // GetSocketSharding returns the SocketSharding field. -func (c Configuration) GetSocketSharding() bool { +func (c *Configuration) GetSocketSharding() bool { return c.SocketSharding } // GetKeepAlive returns the KeepAlive field. -func (c Configuration) GetKeepAlive() time.Duration { +func (c *Configuration) GetKeepAlive() time.Duration { return c.KeepAlive } // GetKeepAlive returns the Timeout field. -func (c Configuration) GetTimeout() time.Duration { +func (c *Configuration) GetTimeout() time.Duration { return c.Timeout } // GetKeepAlive returns the TimeoutMessage field. -func (c Configuration) GetTimeoutMessage() string { +func (c *Configuration) GetTimeoutMessage() string { return c.TimeoutMessage } // GetDisablePathCorrection returns the DisablePathCorrection field. -func (c Configuration) GetDisablePathCorrection() bool { +func (c *Configuration) GetDisablePathCorrection() bool { return c.DisablePathCorrection } // GetDisablePathCorrectionRedirection returns the DisablePathCorrectionRedirection field. -func (c Configuration) GetDisablePathCorrectionRedirection() bool { +func (c *Configuration) GetDisablePathCorrectionRedirection() bool { return c.DisablePathCorrectionRedirection } // GetEnablePathIntelligence returns the EnablePathIntelligence field. -func (c Configuration) GetEnablePathIntelligence() bool { +func (c *Configuration) GetEnablePathIntelligence() bool { return c.EnablePathIntelligence } // GetEnablePathEscape returns the EnablePathEscape field. -func (c Configuration) GetEnablePathEscape() bool { +func (c *Configuration) GetEnablePathEscape() bool { return c.EnablePathEscape } // GetForceLowercaseRouting returns the ForceLowercaseRouting field. -func (c Configuration) GetForceLowercaseRouting() bool { +func (c *Configuration) GetForceLowercaseRouting() bool { return c.ForceLowercaseRouting } // GetFireMethodNotAllowed returns the FireMethodNotAllowed field. -func (c Configuration) GetFireMethodNotAllowed() bool { +func (c *Configuration) GetFireMethodNotAllowed() bool { return c.FireMethodNotAllowed } // GetEnableOptimizations returns the EnableOptimizations. -func (c Configuration) GetEnableOptimizations() bool { +func (c *Configuration) GetEnableOptimizations() bool { return c.EnableOptimizations } +// GetEnableProtoJSON returns the EnableProtoJSON field. +func (c *Configuration) GetEnableProtoJSON() bool { + return c.EnableProtoJSON +} + +// GetEnableEasyJSON returns the EnableEasyJSON field. +func (c *Configuration) GetEnableEasyJSON() bool { + return c.EnableEasyJSON +} + // GetDisableBodyConsumptionOnUnmarshal returns the DisableBodyConsumptionOnUnmarshal field. -func (c Configuration) GetDisableBodyConsumptionOnUnmarshal() bool { +func (c *Configuration) GetDisableBodyConsumptionOnUnmarshal() bool { return c.DisableBodyConsumptionOnUnmarshal } // GetFireEmptyFormError returns the DisableBodyConsumptionOnUnmarshal field. -func (c Configuration) GetFireEmptyFormError() bool { +func (c *Configuration) GetFireEmptyFormError() bool { return c.FireEmptyFormError } // GetDisableAutoFireStatusCode returns the DisableAutoFireStatusCode field. -func (c Configuration) GetDisableAutoFireStatusCode() bool { +func (c *Configuration) GetDisableAutoFireStatusCode() bool { return c.DisableAutoFireStatusCode } // GetResetOnFireErrorCode returns ResetOnFireErrorCode field. -func (c Configuration) GetResetOnFireErrorCode() bool { +func (c *Configuration) GetResetOnFireErrorCode() bool { return c.ResetOnFireErrorCode } // GetTimeFormat returns the TimeFormat field. -func (c Configuration) GetTimeFormat() string { +func (c *Configuration) GetTimeFormat() string { return c.TimeFormat } // GetCharset returns the Charset field. -func (c Configuration) GetCharset() string { +func (c *Configuration) GetCharset() string { return c.Charset } // GetPostMaxMemory returns the PostMaxMemory field. -func (c Configuration) GetPostMaxMemory() int64 { +func (c *Configuration) GetPostMaxMemory() int64 { return c.PostMaxMemory } // GetLocaleContextKey returns the LocaleContextKey field. -func (c Configuration) GetLocaleContextKey() string { +func (c *Configuration) GetLocaleContextKey() string { return c.LocaleContextKey } // GetLanguageContextKey returns the LanguageContextKey field. -func (c Configuration) GetLanguageContextKey() string { +func (c *Configuration) GetLanguageContextKey() string { return c.LanguageContextKey } // GetLanguageInputContextKey returns the LanguageInputContextKey field. -func (c Configuration) GetLanguageInputContextKey() string { +func (c *Configuration) GetLanguageInputContextKey() string { return c.LanguageInputContextKey } // GetVersionContextKey returns the VersionContextKey field. -func (c Configuration) GetVersionContextKey() string { +func (c *Configuration) GetVersionContextKey() string { return c.VersionContextKey } // GetVersionAliasesContextKey returns the VersionAliasesContextKey field. -func (c Configuration) GetVersionAliasesContextKey() string { +func (c *Configuration) GetVersionAliasesContextKey() string { return c.VersionAliasesContextKey } // GetViewEngineContextKey returns the ViewEngineContextKey field. -func (c Configuration) GetViewEngineContextKey() string { +func (c *Configuration) GetViewEngineContextKey() string { return c.ViewEngineContextKey } // GetViewLayoutContextKey returns the ViewLayoutContextKey field. -func (c Configuration) GetViewLayoutContextKey() string { +func (c *Configuration) GetViewLayoutContextKey() string { return c.ViewLayoutContextKey } // GetViewDataContextKey returns the ViewDataContextKey field. -func (c Configuration) GetViewDataContextKey() string { +func (c *Configuration) GetViewDataContextKey() string { return c.ViewDataContextKey } // GetFallbackViewContextKey returns the FallbackViewContextKey field. -func (c Configuration) GetFallbackViewContextKey() string { +func (c *Configuration) GetFallbackViewContextKey() string { return c.FallbackViewContextKey } // GetRemoteAddrHeaders returns the RemoteAddrHeaders field. -func (c Configuration) GetRemoteAddrHeaders() []string { +func (c *Configuration) GetRemoteAddrHeaders() []string { return c.RemoteAddrHeaders } // GetRemoteAddrHeadersForce returns RemoteAddrHeadersForce field. -func (c Configuration) GetRemoteAddrHeadersForce() bool { +func (c *Configuration) GetRemoteAddrHeadersForce() bool { return c.RemoteAddrHeadersForce } // GetSSLProxyHeaders returns the SSLProxyHeaders field. -func (c Configuration) GetSSLProxyHeaders() map[string]string { +func (c *Configuration) GetSSLProxyHeaders() map[string]string { return c.SSLProxyHeaders } // GetRemoteAddrPrivateSubnets returns the RemoteAddrPrivateSubnets field. -func (c Configuration) GetRemoteAddrPrivateSubnets() []netutil.IPRange { +func (c *Configuration) GetRemoteAddrPrivateSubnets() []netutil.IPRange { return c.RemoteAddrPrivateSubnets } // GetHostProxyHeaders returns the HostProxyHeaders field. -func (c Configuration) GetHostProxyHeaders() map[string]bool { +func (c *Configuration) GetHostProxyHeaders() map[string]bool { return c.HostProxyHeaders } // GetOther returns the Other field. -func (c Configuration) GetOther() map[string]interface{} { +func (c *Configuration) GetOther() map[string]interface{} { return c.Other } @@ -1166,6 +1201,14 @@ func WithConfiguration(c Configuration) Configurator { main.EnableOptimizations = v } + if v := c.EnableProtoJSON; v { + main.EnableProtoJSON = v + } + + if v := c.EnableEasyJSON; v { + main.EnableEasyJSON = v + } + if v := c.FireMethodNotAllowed; v { main.FireMethodNotAllowed = v } @@ -1342,6 +1385,8 @@ func DefaultConfiguration() Configuration { SSLProxyHeaders: make(map[string]string), HostProxyHeaders: make(map[string]bool), EnableOptimizations: false, + EnableProtoJSON: false, + EnableEasyJSON: false, Other: make(map[string]interface{}), } } diff --git a/context/application.go b/context/application.go index b376897e..15ce5abb 100644 --- a/context/application.go +++ b/context/application.go @@ -52,6 +52,10 @@ type Application interface { // is hijacked by a third-party middleware and the http handler return too fast. GetContextPool() *Pool + // GetContextErrorHandler returns the handler which handles errors + // on JSON write failures. + GetContextErrorHandler() ErrorHandler + // ServeHTTPC is the internal router, it's visible because it can be used for advanced use cases, // i.e: routing within a foreign context. // diff --git a/context/configuration.go b/context/configuration.go index 67df7e55..7d89f7f1 100644 --- a/context/configuration.go +++ b/context/configuration.go @@ -43,6 +43,11 @@ type ConfigurationReadOnly interface { // GetEnableOptimizations returns the EnableOptimizations field. GetEnableOptimizations() bool + // GetEnableProtoJSON returns the EnableProtoJSON field. + GetEnableProtoJSON() bool + // GetEnableEasyJSON returns the EnableEasyJSON field. + GetEnableEasyJSON() bool + // GetDisableBodyConsumptionOnUnmarshal returns the DisableBodyConsumptionOnUnmarshal field. GetDisableBodyConsumptionOnUnmarshal() bool // GetFireEmptyFormError returns the FireEmptyFormError field. diff --git a/context/context.go b/context/context.go index 257d0bbe..cd497fab 100644 --- a/context/context.go +++ b/context/context.go @@ -3046,6 +3046,9 @@ func (ctx *Context) ReadBody(ptr interface{}) error { // 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. +// +// It reports any write errors back to the caller, Application.SetContentErrorHandler does NOT apply here +// as this is a lower-level method which must be remain as it is. func (ctx *Context) Write(rawBody []byte) (int, error) { return ctx.writer.Write(rawBody) } @@ -3757,38 +3760,20 @@ type ProtoMarshalOptions = protojson.MarshalOptions // JSON contains the options for the JSON (Context's) Renderer. type JSON struct { // http-specific - StreamingJSON bool + StreamingJSON bool `yaml:"StreamingJSON"` // content-specific - UnescapeHTML bool - Indent string - Prefix string - ASCII bool // if true writes with unicode to ASCII content. - Secure bool // if true then it prepends a "while(1);" when Go slice (to JSON Array) value. + UnescapeHTML bool `yaml:"UnescapeHTML"` + Indent string `yaml:"Indent"` + Prefix string `yaml:"Prefix"` + ASCII bool `yaml:"ASCII"` // if true writes with unicode to ASCII content. + Secure bool `yaml:"Secure"` // if true then it prepends a "while(1);" when Go slice (to JSON Array) value. // proto.Message specific marshal options. - Proto ProtoMarshalOptions - - // Optional context cancelation of encoder when Iris optimizations field is enabled. - // On JSON method this is automatically binded to the request context. - Context stdContext.Context - - // ErrorHandler can be optionally registered to fire a customized - // error to the client on JSON write failures. - ErrorHandler ErrorHandler + Proto ProtoMarshalOptions `yaml:"ProtoMarshalOptions"` } -type ( - // ErrorHandler describes a context error handler. As for today this is only used - // to globally or per-party or per-route handle JSON writes error. - ErrorHandler interface { - HandleContextError(ctx *Context, err error) - } - // ErrorHandlerFunc a function shortcut for ErrorHandler interface. - ErrorHandlerFunc func(ctx *Context, err error) -) - -func (h ErrorHandlerFunc) HandleContextError(ctx *Context, err error) { - h(ctx, err) -} +// DefaultJSONOptions is the optional settings that are being used +// inside `Context.JSON`. +var DefaultJSONOptions = JSON{} // IsDefault reports whether this JSON options structure holds the default values. func (j *JSON) IsDefault() bool { @@ -3799,16 +3784,6 @@ func (j *JSON) IsDefault() bool { j.ASCII == DefaultJSONOptions.ASCII && j.Secure == DefaultJSONOptions.Secure && j.Proto == DefaultJSONOptions.Proto - // except context and error handler -} - -// GetContext returns the option's Context or the HTTP request's one. -func (j *JSON) GetContext(ctx *Context) stdContext.Context { - if j.Context == nil { - return ctx.request.Context() - } - - return j.Context } // JSONP contains the options for the JSONP (Context's) Renderer. @@ -3849,32 +3824,64 @@ var ( secureJSONPrefix = []byte("while(1);") ) -func handleJSONResponseValue(w io.Writer, v interface{}, options JSON) (bool, int, error) { - if m, ok := v.(proto.Message); ok { - result, err := options.Proto.Marshal(m) - if err != nil { - return true, 0, err - } +func (ctx *Context) handleSpecialJSONResponseValue(v interface{}, options *JSON) (bool, int, error) { + if ctx.app.ConfigurationReadOnly().GetEnableProtoJSON() { + if m, ok := v.(proto.Message); ok { + protoJSON := ProtoMarshalOptions{} + if options != nil { + protoJSON = options.Proto + } - n, err := w.Write(result) - return true, n, err + result, err := protoJSON.Marshal(m) + if err != nil { + return true, 0, err + } + + n, err := ctx.writer.Write(result) + return true, n, err + } } - if easyObject, ok := v.(easyjson.Marshaler); ok { - jw := jwriter.Writer{NoEscapeHTML: !options.UnescapeHTML} - easyObject.MarshalEasyJSON(&jw) - n, err := jw.DumpTo(w) - return true, n, err + if ctx.app.ConfigurationReadOnly().GetEnableEasyJSON() { + if easyObject, ok := v.(easyjson.Marshaler); ok { + noEscapeHTML := false + if options != nil { + noEscapeHTML = !options.UnescapeHTML + } + jw := jwriter.Writer{NoEscapeHTML: noEscapeHTML} + easyObject.MarshalEasyJSON(&jw) + + n, err := jw.DumpTo(ctx.writer) + return true, n, err + } } return false, 0, nil } // WriteJSON marshals the given interface object and writes the JSON response to the 'writer'. -// Ignores StatusCode and StreamingJSON options. -func WriteJSON(writer io.Writer, v interface{}, options JSON, shouldOptimize bool) (int, error) { - if handled, n, err := handleJSONResponseValue(writer, v, options); handled { - return n, err +func WriteJSON(ctx stdContext.Context, writer io.Writer, v interface{}, options *JSON, shouldOptimize bool) (int, error) { + if options.StreamingJSON { + var err error + if shouldOptimize { + // jsoniterConfig := jsoniter.Config{ + // EscapeHTML: !options.UnescapeHTML, + // IndentionStep: 4, + // }.Froze() + // enc := jsoniterConfig.NewEncoder(ctx.writer) + // err = enc.Encode(v) + enc := gojson.NewEncoder(writer) + enc.SetEscapeHTML(!options.UnescapeHTML) + enc.SetIndent(options.Prefix, options.Indent) + err = enc.EncodeContext(ctx, v) + } else { + enc := json.NewEncoder(writer) + enc.SetEscapeHTML(!options.UnescapeHTML) + enc.SetIndent(options.Prefix, options.Indent) + err = enc.Encode(v) + } + + return 0, err } var ( @@ -3899,8 +3906,8 @@ func WriteJSON(writer io.Writer, v interface{}, options JSON, shouldOptimize boo } else { if shouldOptimize { // result, err = jsoniter.ConfigCompatibleWithStandardLibrary.Marshal - if options.Context != nil { - result, err = gojson.MarshalContext(options.Context, v) + if ctx != nil { + result, err = gojson.MarshalContext(ctx, v) } else { result, err = gojson.Marshal(v) } @@ -3939,7 +3946,7 @@ func WriteJSON(writer io.Writer, v interface{}, options JSON, shouldOptimize boo if options.ASCII { if len(result) > 0 { buf := new(bytes.Buffer) - for _, s := range bytesToString(result) { + for _, s := range string(result) { char := string(s) if s >= 128 { char = fmt.Sprintf("\\u%04x", int64(s)) @@ -3967,123 +3974,84 @@ func stringToBytes(s string) []byte { return *(*[]byte)(unsafe.Pointer(&s)) } -// DefaultJSONOptions is the optional settings that are being used -// inside `ctx.JSON`. -var DefaultJSONOptions = JSON{} +type ( + // ErrorHandler describes a context error handler which applies on + // JSON, JSONP, Protobuf, MsgPack, XML, YAML and Markdown write errors. + // + // An ErrorHandler can be registered once via Application.SetErrorHandler method to override the default behavior. + // The default behavior is to simply send status internal code error + // without a body back to the client. + ErrorHandler interface { + HandleContextError(ctx *Context, err error) + } + // ErrorHandlerFunc a function shortcut for ErrorHandler interface. + ErrorHandlerFunc func(ctx *Context, err error) +) -const jsonOptionsContextKey = "iris.context.json_options" - -// SetJSONOptions stores the given JSON options to the handler -// for any next Context.JSON calls. Note that the Context.JSON's -// variadic options have priority over these given options. -// -// Usage Example: -// -// type jsonErrorHandler struct{} -// func (e *jsonErrorHandler) HandleContextError(ctx iris.Context, err error) { -// errors.InvalidArgument.Err(ctx, err) -// } -// ... -// errHandler := new(jsonErrorHandler) -// srv.Use(func(ctx iris.Context) { -// ctx.SetJSONOptions(iris.JSON{ -// ErrorHandler: errHandler, -// }) -// ctx.Next() -// }) -func (ctx *Context) SetJSONOptions(opts JSON) { - ctx.values.Set(jsonOptionsContextKey, opts) +// HandleContextError completes the ErrorHandler interface. +func (h ErrorHandlerFunc) HandleContextError(ctx *Context, err error) { + h(ctx, err) } -func (ctx *Context) getJSONOptions() (JSON, bool) { - if v := ctx.values.Get(jsonOptionsContextKey); v != nil { - opts, ok := v.(JSON) - return opts, ok +func (ctx *Context) handleContextError(err error) { + if errHandler := ctx.app.GetContextErrorHandler(); errHandler != nil { + errHandler.HandleContextError(ctx, err) + } else { + ctx.StatusCode(http.StatusInternalServerError) } - return DefaultJSONOptions, false + // keep the error non nil so the caller has control over further actions. } -// JSON marshals the given interface object and writes the JSON response to the client. -// If the value is a compatible `proto.Message` one -// then it only uses the options.Proto settings to marshal. +// JSON marshals the given "v" value to JSON and writes the response to the client. +// Look the Configuration.EnableProtoJSON/EnableEasyJSON and EnableOptimizations too. +// +// It reports any JSON parser or write errors back to the caller. +// Look the Application.SetContextErrorHandler to override the +// default status code 500 with a custom error response. +// +// It can, optionally, accept the JSON structure which may hold customizations over the +// final JSON response but keep in mind that the caller should NOT modify that JSON options +// value in another goroutine while JSON method is still running. func (ctx *Context) JSON(v interface{}, opts ...JSON) (n int, err error) { - if n, err = ctx.writeJSON(v, opts...); err != nil { - if opts, ok := ctx.getJSONOptions(); ok { - opts.ErrorHandler.HandleContextError(ctx, err) - } // keep the error so the caller has control over further actions. + var options *JSON + if len(opts) > 0 { + options = &opts[0] + } + + if n, err = ctx.writeJSON(v, options); err != nil { + ctx.handleContextError(err) } return } -func (ctx *Context) writeJSON(v interface{}, opts ...JSON) (n int, err error) { +func (ctx *Context) writeJSON(v interface{}, options *JSON) (int, error) { ctx.ContentType(ContentJSONHeaderValue) + + // After content type given and before everything else, try handle proto or easyjson, no matter the performance mode. + if handled, n, err := ctx.handleSpecialJSONResponseValue(v, options); handled { + return n, err + } + shouldOptimize := ctx.shouldOptimize() - - options := DefaultJSONOptions - optsLength := len(opts) - if optsLength > 0 { - options = opts[0] - } else { - if opt, ok := ctx.getJSONOptions(); ok { - options = opt - if !options.IsDefault() { // keep the next branch valid when only the Context or/and ErrorHandler are modified. - optsLength = 1 - } - } - } - - if shouldOptimize && optsLength == 0 { // if no options given and optimizations are enabled. - // try handle proto or easyjson. - if handled, n, err := handleJSONResponseValue(ctx, v, options); handled { - return n, err - } - - // as soon as possible, use the fast json marshaler with the http request context. - result, err := gojson.MarshalContext(ctx.request.Context(), v) - if err != nil { - return 0, err - } - - return ctx.Write(result) - } - - if options.StreamingJSON { + if options == nil { if shouldOptimize { - // jsoniterConfig := jsoniter.Config{ - // EscapeHTML: !options.UnescapeHTML, - // IndentionStep: 4, - // }.Froze() - // enc := jsoniterConfig.NewEncoder(ctx.writer) - // err = enc.Encode(v) - enc := gojson.NewEncoder(ctx.writer) - enc.SetEscapeHTML(!options.UnescapeHTML) - enc.SetIndent(options.Prefix, options.Indent) - err = enc.EncodeContext(options.GetContext(ctx), v) - } else { - enc := json.NewEncoder(ctx.writer) - enc.SetEscapeHTML(!options.UnescapeHTML) - enc.SetIndent(options.Prefix, options.Indent) - err = enc.Encode(v) + // If no options given and optimizations are enabled. + // write using the fast json marshaler with the http request context as soon as possible. + result, err := gojson.MarshalContext(ctx.request.Context(), v) + if err != nil { + return 0, err + } + + return ctx.Write(result) } - if err != nil { - ctx.app.Logger().Debugf("JSON: %v", err) - ctx.StatusCode(http.StatusInternalServerError) // it handles the fallback to normal mode here which also removes any compression headers. - return 0, err - } - return ctx.writer.Written(), err + // Else if no options given neither optimizations are enabled, then safely read the already-initialized object. + options = &DefaultJSONOptions } - n, err = WriteJSON(ctx.writer, v, options, shouldOptimize) - if err != nil { - ctx.app.Logger().Debugf("JSON: %v", err) - ctx.StatusCode(http.StatusInternalServerError) - return 0, err - } - - return n, err + return WriteJSON(ctx, ctx.writer, v, options, shouldOptimize) } var finishCallbackB = []byte(");") @@ -4134,24 +4102,23 @@ func WriteJSONP(writer io.Writer, v interface{}, options JSONP, optimize bool) ( // inside `ctx.JSONP`. var DefaultJSONPOptions = JSONP{} -// JSONP marshals the given interface object and writes the JSON response to the client. -func (ctx *Context) JSONP(v interface{}, opts ...JSONP) (int, error) { +// JSONP marshals the given "v" value to JSON and sends the response to the client. +// +// It reports any JSON parser or write errors back to the caller. +// Look the Application.SetContextErrorHandler to override the +// default status code 500 with a custom error response. +func (ctx *Context) JSONP(v interface{}, opts ...JSONP) (n int, err error) { options := DefaultJSONPOptions - if len(opts) > 0 { options = opts[0] } ctx.ContentType(ContentJavascriptHeaderValue) - - n, err := WriteJSONP(ctx.writer, v, options, ctx.shouldOptimize()) - if err != nil { - ctx.app.Logger().Debugf("JSONP: %v", err) - ctx.StatusCode(http.StatusInternalServerError) - return 0, err + if n, err = WriteJSONP(ctx.writer, v, options, ctx.shouldOptimize()); err != nil { + ctx.handleContextError(err) } - return n, err + return } type xmlMapEntry struct { @@ -4232,29 +4199,28 @@ var DefaultXMLOptions = XML{} // XML marshals the given interface object and writes the XML response to the client. // To render maps as XML see the `XMLMap` package-level function. -func (ctx *Context) XML(v interface{}, opts ...XML) (int, error) { +// +// It reports any XML parser or write errors back to the caller. +// Look the Application.SetContextErrorHandler to override the +// default status code 500 with a custom error response. +func (ctx *Context) XML(v interface{}, opts ...XML) (n int, err error) { options := DefaultXMLOptions - if len(opts) > 0 { options = opts[0] } ctx.ContentType(ContentXMLHeaderValue) - - n, err := WriteXML(ctx.writer, v, options, ctx.shouldOptimize()) - if err != nil { - ctx.app.Logger().Debugf("XML: %v", err) - ctx.StatusCode(http.StatusInternalServerError) - return 0, err + if n, err = WriteXML(ctx.writer, v, options, ctx.shouldOptimize()); err != nil { + ctx.handleContextError(err) } - return n, err + return } // Problem writes a JSON or XML problem response. // Order of Problem fields are not always rendered the same. // -// Behaves exactly like `Context.JSON` +// Behaves exactly like the `Context.JSON` method // but with default ProblemOptions.JSON indent of " " and // a response content type of "application/problem+json" instead. // @@ -4299,11 +4265,12 @@ func (ctx *Context) Problem(v interface{}, opts ...ProblemOptions) (int, error) } // WriteMarkdown parses the markdown to html and writes these contents to the writer. -func WriteMarkdown(writer io.Writer, markdownB []byte, options Markdown) (int, error) { +func WriteMarkdown(writer io.Writer, markdownB []byte, options Markdown) (n int, err error) { buf := blackfriday.Run(markdownB) if options.Sanitize { buf = bluemonday.UGCPolicy().SanitizeBytes(buf) } + return writer.Write(buf) } @@ -4312,66 +4279,90 @@ func WriteMarkdown(writer io.Writer, markdownB []byte, options Markdown) (int, e var DefaultMarkdownOptions = Markdown{} // Markdown parses the markdown to html and renders its result to the client. -func (ctx *Context) Markdown(markdownB []byte, opts ...Markdown) (int, error) { +// +// It reports any Markdown parser or write errors back to the caller. +// Look the Application.SetContextErrorHandler to override the +// default status code 500 with a custom error response. +func (ctx *Context) Markdown(markdownB []byte, opts ...Markdown) (n int, err error) { options := DefaultMarkdownOptions - if len(opts) > 0 { options = opts[0] } ctx.ContentType(ContentHTMLHeaderValue) + if n, err = WriteMarkdown(ctx.writer, markdownB, options); err != nil { + ctx.handleContextError(err) + } - n, err := WriteMarkdown(ctx.writer, markdownB, options) + return +} + +// YAML marshals the given "v" value using the yaml marshaler and writes the result to the client. +// +// It reports any YAML parser or write errors back to the caller. +// Look the Application.SetContextErrorHandler to override the +// default status code 500 with a custom error response. +func (ctx *Context) YAML(v interface{}) (int, error) { + out, err := yaml.Marshal(v) if err != nil { - ctx.app.Logger().Debugf("Markdown: %v", err) - ctx.StatusCode(http.StatusInternalServerError) + ctx.handleContextError(err) return 0, err } + ctx.ContentType(ContentYAMLHeaderValue) + n, err := ctx.Write(out) + if err != nil { + ctx.handleContextError(err) + } + return n, err } -// YAML marshals the "v" using the yaml marshaler -// and sends the result to the client. -func (ctx *Context) YAML(v interface{}) (int, error) { - out, err := yaml.Marshal(v) - if err != nil { - ctx.app.Logger().Debugf("YAML: %v", err) - ctx.StatusCode(http.StatusInternalServerError) - return 0, err - } - - ctx.ContentType(ContentYAMLHeaderValue) - return ctx.Write(out) -} - -// TextYAML marshals the "v" using the yaml marshaler -// and renders to the client. +// TextYAML calls the Context.YAML method but with the text/yaml content type instead. func (ctx *Context) TextYAML(v interface{}) (int, error) { ctx.contentTypeOnce(ContentYAMLTextHeaderValue, "") return ctx.YAML(v) } -// Protobuf parses the "v" of proto Message and renders its result to the client. +// Protobuf marshals the given "v" value of proto Message and writes its result to the client. +// +// It reports any protobuf parser or write errors back to the caller. +// Look the Application.SetContextErrorHandler to override the +// default status code 500 with a custom error response. func (ctx *Context) Protobuf(v proto.Message) (int, error) { out, err := proto.Marshal(v) if err != nil { + ctx.handleContextError(err) return 0, err } ctx.ContentType(ContentProtobufHeaderValue) - return ctx.Write(out) + n, err := ctx.Write(out) + if err != nil { + ctx.handleContextError(err) + } + + return n, err } -// MsgPack parses the "v" of msgpack format and renders its result to the client. +// MsgPack marshals the given "v" value of msgpack format and writes its result to the client. +// +// It reports any message pack or write errors back to the caller. +// Look the Application.SetContextErrorHandler to override the +// default status code 500 with a custom error response. func (ctx *Context) MsgPack(v interface{}) (int, error) { out, err := msgpack.Marshal(v) if err != nil { - return 0, err + ctx.handleContextError(err) } ctx.ContentType(ContentMsgPackHeaderValue) - return ctx.Write(out) + n, err := ctx.Write(out) + if err != nil { + ctx.handleContextError(err) + } + + return n, err } // +-----------------------------------------------------------------------+ diff --git a/iris.go b/iris.go index 0405d763..c28a7cdf 100644 --- a/iris.go +++ b/iris.go @@ -59,6 +59,8 @@ type Application struct { *router.Router router.HTTPErrorHandler // if Router is Downgraded this is nil. ContextPool *context.Pool + // See SetContextErrorHandler, defaults to nil. + contextErrorHandler context.ErrorHandler // config contains the configuration fields // all fields defaults to something that is working, developers don't have to set it. @@ -429,6 +431,28 @@ func (app *Application) GetContextPool() *context.Pool { return app.ContextPool } +// SetContextErrorHandler can optionally register a handler to handle +// and fire a customized error body to the client on JSON write failures. +// +// ExampleCode: +// +// type contextErrorHandler struct{} +// func (e *contextErrorHandler) HandleContextError(ctx iris.Context, err error) { +// errors.InvalidArgument.Err(ctx, err) +// } +// ... +// app.SetContextErrorHandler(new(contextErrorHandler)) +func (app *Application) SetContextErrorHandler(errHandler context.ErrorHandler) *Application { + app.contextErrorHandler = errHandler + return app +} + +// GetContextErrorHandler returns the handler which handles errors +// on JSON write failures. +func (app *Application) GetContextErrorHandler() context.ErrorHandler { + return app.contextErrorHandler +} + // ConfigureHost accepts one or more `host#Configuration`, these configurators functions // can access the host created by `app.Run` or `app.Listen`, // they're being executed when application is ready to being served to the public. From 3582427df6eb049e0c0696cc32be0ab0f0566b94 Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Wed, 13 Apr 2022 01:11:31 +0300 Subject: [PATCH 17/20] add an example of SetContextErrorHandler --- _examples/response-writer/write-rest/main.go | 14 +++++++++++--- context/context.go | 6 +++--- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/_examples/response-writer/write-rest/main.go b/_examples/response-writer/write-rest/main.go index db9e9bfa..160144f2 100644 --- a/_examples/response-writer/write-rest/main.go +++ b/_examples/response-writer/write-rest/main.go @@ -4,6 +4,7 @@ import ( "encoding/xml" "github.com/kataras/iris/v12" + "github.com/kataras/iris/v12/x/errors" ) // User example struct for json and msgpack. @@ -29,15 +30,16 @@ type ExampleYAML struct { func main() { app := iris.New() - + // Optionally, set a custom handler for JSON, JSONP, Protobuf, MsgPack, YAML, Markdown... + // write errors. + app.SetContextErrorHandler(new(errorHandler)) // Read app.Post("/decode", func(ctx iris.Context) { // Read https://github.com/kataras/iris/blob/master/_examples/request-body/read-json/main.go as well. var user User err := ctx.ReadJSON(&user) if err != nil { - ctx.StatusCode(iris.StatusBadRequest) - ctx.Writef("unable to read body: %s\nbody is empty: %v", err.Error(), iris.IsErrEmptyJSON(err)) + errors.InvalidArgument.Details(ctx, "unable to parse body", err.Error()) return } @@ -165,3 +167,9 @@ func main() { // if passed to the `Run` then it will not print its passed error as an actual server error. app.Listen(":8080", iris.WithOptimizations) } + +type errorHandler struct{} + +func (h *errorHandler) HandleContextError(ctx iris.Context, err error) { + errors.Internal.Err(ctx, err) +} diff --git a/context/context.go b/context/context.go index cd497fab..fe7fa6f3 100644 --- a/context/context.go +++ b/context/context.go @@ -3966,9 +3966,9 @@ func WriteJSON(ctx stdContext.Context, writer io.Writer, v interface{}, options } // See https://golang.org/src/strings/builder.go#L45 -func bytesToString(b []byte) string { - return *(*string)(unsafe.Pointer(&b)) -} +// func bytesToString(b []byte) string { +// return *(*string)(unsafe.Pointer(&b)) +// } func stringToBytes(s string) []byte { return *(*[]byte)(unsafe.Pointer(&s)) From ecb1c617df397e2eafe788c7e8a6c6e4c066c670 Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Wed, 13 Apr 2022 02:25:47 +0300 Subject: [PATCH 18/20] handle https://github.com/kataras/iris/issues/1875 --- HISTORY.md | 1 + _examples/file-server/webdav/main.go | 2 +- _examples/request-body/read-query/main.go | 4 +- aliases.go | 31 ++++++++++++ iris.go | 62 ++++++++++++++++++++--- 5 files changed, 91 insertions(+), 9 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index d3afa52c..237cfa66 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -28,6 +28,7 @@ The codebase for Dependency Injection, Internationalization and localization and ## Fixes and Improvements +- Add `iris.AllowQuerySemicolons` and `iris.WithoutServerError(iris.ErrURLQuerySemicolon)` to handle golang.org/issue/25192 as reported at: https://github.com/kataras/iris/issues/1875. - Add new `Application.SetContextErrorHandler` to globally customize the default behavior (status code 500 without body) on `JSON`, `JSONP`, `Protobuf`, `MsgPack`, `XML`, `YAML` and `Markdown` method call write errors instead of catching the error on each handler. - Add new [x/pagination](x/pagination/pagination.go) sub-package which supports generics code (go 1.18+). - Add new [middleware/modrevision](middleware/modrevision) middleware (example at [_examples/project/api/router.go]_examples/project/api/router.go). diff --git a/_examples/file-server/webdav/main.go b/_examples/file-server/webdav/main.go index 0db07792..be981a8e 100644 --- a/_examples/file-server/webdav/main.go +++ b/_examples/file-server/webdav/main.go @@ -32,7 +32,7 @@ func main() { app.HandleMany(strings.Join(iris.WebDAVMethods, " "), "/{p:path}", iris.FromStd(webdavHandler)) app.Listen(":8080", - iris.WithoutServerError(iris.ErrServerClosed), + iris.WithoutServerError(iris.ErrServerClosed, iris.ErrURLQuerySemicolon), iris.WithoutPathCorrection, ) } diff --git a/_examples/request-body/read-query/main.go b/_examples/request-body/read-query/main.go index e8b763a5..e8c0de54 100644 --- a/_examples/request-body/read-query/main.go +++ b/_examples/request-body/read-query/main.go @@ -13,6 +13,7 @@ type MyType struct { func main() { app := iris.New() + app.UseRouter(iris.AllowQuerySemicolons) // Optionally: to restore pre go1.17 behavior of url parsing. app.Get("/", func(ctx iris.Context) { var t MyType @@ -45,5 +46,6 @@ func main() { // http://localhost:8080/simple?name=john&name=doe&name=kataras // // Note: this `WithEmptyFormError` will give an error if the query was empty. - app.Listen(":8080", iris.WithEmptyFormError) + app.Listen(":8080", iris.WithEmptyFormError, + iris.WithoutServerError(iris.ErrServerClosed, iris.ErrURLQuerySemicolon)) } diff --git a/aliases.go b/aliases.go index 76968a66..a686410c 100644 --- a/aliases.go +++ b/aliases.go @@ -2,8 +2,10 @@ package iris import ( "net/http" + "net/url" "path" "regexp" + "strings" "github.com/kataras/iris/v12/cache" "github.com/kataras/iris/v12/context" @@ -328,6 +330,35 @@ var ( ctx.Next() } + // AllowQuerySemicolons returns a middleware that serves requests by converting any + // unescaped semicolons(;) in the URL query to ampersands(&). + // + // This restores the pre-Go 1.17 behavior of splitting query parameters on both + // semicolons and ampersands. + // (See golang.org/issue/25192 and https://github.com/kataras/iris/issues/1875). + // Note that this behavior doesn't match that of many proxies, + // and the mismatch can lead to security issues. + // + // AllowQuerySemicolons should be invoked before any Context read query or + // form methods are called. + // + // To skip HTTP Server logging for this type of warning: + // app.Listen/Run(..., iris.WithoutServerError(iris.ErrURLQuerySemicolon)). + AllowQuerySemicolons = func(ctx Context) { + // clopy of net/http.AllowQuerySemicolons. + r := ctx.Request() + if s := r.URL.RawQuery; strings.Contains(s, ";") { + r2 := new(http.Request) + *r2 = *r + r2.URL = new(url.URL) + *r2.URL = *r.URL + r2.URL.RawQuery = strings.ReplaceAll(s, ";", "&") + ctx.ResetRequest(r2) + } + + ctx.Next() + } + // MatchImagesAssets is a simple regex expression // that can be passed to the DirOptions.Cache.CompressIgnore field // in order to skip compression on already-compressed file types diff --git a/iris.go b/iris.go index c28a7cdf..f5acfd84 100644 --- a/iris.go +++ b/iris.go @@ -1,6 +1,7 @@ package iris import ( + "bytes" stdContext "context" "errors" "fmt" @@ -475,6 +476,40 @@ func (app *Application) ConfigureHost(configurators ...host.Configurator) *Appli return app } +const serverLoggerPrefix = "[HTTP Server] " + +type customHostServerLogger struct { // see #1875 + parent io.Writer + ignoreLogs [][]byte +} + +var newLineBytes = []byte("\n") + +func newCustomHostServerLogger(w io.Writer, ignoreLogs []string) *customHostServerLogger { + prefixAsByteSlice := []byte(serverLoggerPrefix) + + // build the ignore lines. + ignoreLogsAsByteSlice := make([][]byte, 0, len(ignoreLogs)) + for _, s := range ignoreLogs { + ignoreLogsAsByteSlice = append(ignoreLogsAsByteSlice, append(prefixAsByteSlice, []byte(s)...)) // append([]byte(s), newLineBytes...) + } + + return &customHostServerLogger{ + parent: w, + ignoreLogs: ignoreLogsAsByteSlice, + } +} + +func (l *customHostServerLogger) Write(p []byte) (int, error) { + for _, ignoredLogBytes := range l.ignoreLogs { + if bytes.Equal(bytes.TrimSuffix(p, newLineBytes), ignoredLogBytes) { + return 0, nil + } + } + + return l.parent.Write(p) +} + // NewHost accepts a standard *http.Server object, // completes the necessary missing parts of that "srv" // and returns a new, ready-to-use, host (supervisor). @@ -487,9 +522,10 @@ func (app *Application) NewHost(srv *http.Server) *host.Supervisor { srv.Handler = app.Router } - // check if different ErrorLog provided, if not bind it with the framework's logger + // check if different ErrorLog provided, if not bind it with the framework's logger. if srv.ErrorLog == nil { - srv.ErrorLog = log.New(app.logger.Printer.Output, "[HTTP Server] ", 0) + serverLogger := newCustomHostServerLogger(app.logger.Printer.Output, app.config.IgnoreServerErrors) + srv.ErrorLog = log.New(serverLogger, serverLoggerPrefix, 0) } if addr := srv.Addr; addr == "" { @@ -913,11 +949,23 @@ func Raw(f func() error) Runner { } } -// ErrServerClosed is returned by the Server's Serve, ServeTLS, ListenAndServe, -// and ListenAndServeTLS methods after a call to Shutdown or Close. -// -// A shortcut for the `http#ErrServerClosed`. -var ErrServerClosed = http.ErrServerClosed +var ( + // ErrServerClosed is logged by the standard net/http server when the server is terminated. + // Ignore it by passing this error to the `iris.WithoutServerError` configurator + // on `Application.Run/Listen` method. + // + // An alias of the `http#ErrServerClosed`. + ErrServerClosed = http.ErrServerClosed + + // ErrURLQuerySemicolon is logged by the standard net/http server when + // the request contains a semicolon (;) wihch, after go1.17 it's not used as a key-value separator character. + // + // Ignore it by passing this error to the `iris.WithoutServerError` configurator + // on `Application.Run/Listen` method. + // + // An alias of the `http#ErrServerClosed`. + ErrURLQuerySemicolon = errors.New("http: URL query contains semicolon, which is no longer a supported separator; parts of the query may be stripped when parsed; see golang.org/issue/25192") +) // Listen builds the application and starts the server // on the TCP network address "host:port" which From 373adbd8c03cf07df0c51b348d28ac034e256480 Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Wed, 13 Apr 2022 02:39:09 +0300 Subject: [PATCH 19/20] version 12.2.0-beta1 --- README.md | 20 +++----------------- doc.go | 4 ++-- iris.go | 2 +- 3 files changed, 6 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index f47c2836..e5f36cd4 100644 --- a/README.md +++ b/README.md @@ -281,7 +281,7 @@ Venkatt Guhesan" title="vguhesan" with="75" style="width:75px;max-width:75px;hei $ mkdir myapp $ cd myapp $ go mod init myapp -$ go get github.com/kataras/iris/v12@master # or @v12.2.0-alpha8 +$ go get github.com/kataras/iris/v12@master # or @v12.2.0-beta1 ```
Install on existing project @@ -291,25 +291,11 @@ $ cd myapp $ go get github.com/kataras/iris/v12@master ``` -
- -
Install with a go.mod file - -```txt -module myapp - -go 1.16 - -require github.com/kataras/iris/v12 master -``` - **Run** ```sh -$ go mod download -$ go run main.go -# OR just: -# go run -mod=mod main.go +$ go mod tidy -compat=1.18 +$ go run . ```
diff --git a/doc.go b/doc.go index ff7d77c7..de79828f 100644 --- a/doc.go +++ b/doc.go @@ -38,11 +38,11 @@ Source code and other details for the project are available at GitHub: Current Version -12.2.0-alpha9 +12.2.0-beta1 Installation -The only requirement is the Go Programming Language, at least version 1.17. +The only requirement is the Go Programming Language, at least version 1.18. $ go get github.com/kataras/iris/v12@master diff --git a/iris.go b/iris.go index f5acfd84..bcd23b68 100644 --- a/iris.go +++ b/iris.go @@ -39,7 +39,7 @@ import ( ) // Version is the current version of the Iris Web Framework. -const Version = "12.2.0-alpha9" +const Version = "12.2.0-beta1" // Byte unit helpers. const ( From a36bd7c235e7cbb9bb6dd90006bb45a110f39c8b Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Wed, 13 Apr 2022 02:44:49 +0300 Subject: [PATCH 20/20] update deps --- go.mod | 18 +++++++++--------- go.sum | 36 ++++++++++++++++++------------------ 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/go.mod b/go.mod index 147b0402..883ac323 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.18 // retract v12.1.8 // please update to @master require ( - github.com/BurntSushi/toml v1.0.0 + github.com/BurntSushi/toml v1.1.0 github.com/CloudyKit/jet/v6 v6.1.0 github.com/Shopify/goreferrer v0.0.0-20210630161223-536fa16abd6f github.com/andybalholm/brotli v1.0.4 @@ -15,7 +15,7 @@ require ( github.com/fatih/structs v1.1.0 github.com/flosch/pongo2/v4 v4.0.2 github.com/go-redis/redis/v8 v8.11.5 - github.com/goccy/go-json v0.9.7-0.20220325155717-3a4ad3198047 + github.com/goccy/go-json v0.9.7-0.20220412154129-171d97575378 github.com/golang/snappy v0.0.4 github.com/google/uuid v1.3.0 github.com/gorilla/securecookie v1.1.1 @@ -37,15 +37,15 @@ require ( github.com/russross/blackfriday/v2 v2.1.0 github.com/schollz/closestmatch v2.1.0+incompatible github.com/shirou/gopsutil/v3 v3.22.3 - github.com/tdewolff/minify/v2 v2.10.0 + github.com/tdewolff/minify/v2 v2.11.1 github.com/vmihailenco/msgpack/v5 v5.3.5 github.com/yosssi/ace v0.0.5 go.etcd.io/bbolt v1.3.6 - golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 - golang.org/x/net v0.0.0-20220401154927-543a649e0bdd - golang.org/x/sys v0.0.0-20220330033206-e17cdc41300f + golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 + golang.org/x/net v0.0.0-20220412020605-290c469a71a5 + golang.org/x/sys v0.0.0-20220412211240-33da011f77ad golang.org/x/text v0.3.7 - golang.org/x/time v0.0.0-20220224211638-0e9765cccd65 + golang.org/x/time v0.0.0-20220411224347-583f2d630306 google.golang.org/protobuf v1.28.0 gopkg.in/ini.v1 v1.66.4 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b @@ -69,7 +69,7 @@ require ( github.com/gobwas/ws v1.1.0 // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/google/go-querystring v1.1.0 // indirect - github.com/gopherjs/gopherjs v0.0.0-20220221023154-0b2280d3ff96 // indirect + github.com/gopherjs/gopherjs v0.0.0-20220410123724-9e86199038b0 // indirect github.com/gorilla/css v1.0.0 // indirect github.com/gorilla/websocket v1.5.0 // indirect github.com/imkira/go-interpol v1.1.0 // indirect @@ -93,7 +93,7 @@ require ( github.com/sirupsen/logrus v1.8.1 // indirect github.com/smartystreets/assertions v1.2.1 // indirect github.com/stretchr/testify v1.7.1 // indirect - github.com/tdewolff/parse/v2 v2.5.27 // indirect + github.com/tdewolff/parse/v2 v2.5.28 // indirect github.com/tklauser/go-sysconf v0.3.10 // indirect github.com/tklauser/numcpus v0.4.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect diff --git a/go.sum b/go.sum index 20039abc..78e44be9 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,6 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/toml v1.0.0 h1:dtDWrepsVPfW9H/4y7dDgFc2MBUSeJhlaDtK13CxFlU= -github.com/BurntSushi/toml v1.0.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I= +github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 h1:sR+/8Yb4slttB4vD+b9btVEnWgL3Q00OBTzVT8B9C0c= github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno= github.com/CloudyKit/jet/v6 v6.1.0 h1:hvO96X345XagdH1fAoBjpBYG4a1ghhL/QzalkduPuXk= @@ -62,8 +62,8 @@ github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.1.0 h1:7RFti/xnNkMJnrK7D1yQ/iCIB5OrrY/54/H930kIbHA= github.com/gobwas/ws v1.1.0/go.mod h1:nzvNcVha5eUziGrbxFCo6qFIojQHjJV5cLYIbezhfL0= -github.com/goccy/go-json v0.9.7-0.20220325155717-3a4ad3198047 h1:SMQ4NGzEnbUgyY0ids2HuBTOFSUPOjL3GRh5l7zwrvk= -github.com/goccy/go-json v0.9.7-0.20220325155717-3a4ad3198047/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/goccy/go-json v0.9.7-0.20220412154129-171d97575378 h1:eIZ4l5hJq4PBURyWol+fDlr2dFNFYIIvePwkAnk3jws= +github.com/goccy/go-json v0.9.7-0.20220412154129-171d97575378/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= @@ -81,8 +81,8 @@ github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gopherjs/gopherjs v0.0.0-20220221023154-0b2280d3ff96 h1:QJq7UBOuoynsywLk+aC75rC2Cbi2+lQRDaLaizhA+fA= -github.com/gopherjs/gopherjs v0.0.0-20220221023154-0b2280d3ff96/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k= +github.com/gopherjs/gopherjs v0.0.0-20220410123724-9e86199038b0 h1:fWY+zXdWhvWndXqnMj4SyC/vi8sK508OjhGCtMzsA9M= +github.com/gopherjs/gopherjs v0.0.0-20220410123724-9e86199038b0/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k= github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= @@ -204,10 +204,10 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/tdewolff/minify/v2 v2.10.0 h1:ovVAHUcjfGrBDf1EIvsodRUVJiZK/28mMose08B7k14= -github.com/tdewolff/minify/v2 v2.10.0/go.mod h1:6XAjcHM46pFcRE0eztigFPm0Q+Cxsw8YhEWT+rDkcZM= -github.com/tdewolff/parse/v2 v2.5.27 h1:PL3LzzXaOpmdrknnOlIeO2muIBHAwiKp6TxN1RbU5gI= -github.com/tdewolff/parse/v2 v2.5.27/go.mod h1:WzaJpRSbwq++EIQHYIRTpbYKNA3gn9it1Ik++q4zyho= +github.com/tdewolff/minify/v2 v2.11.1 h1:x2IAGnHs3qBjulArA7g4dYGCpcMrM8H2sywfwr436RA= +github.com/tdewolff/minify/v2 v2.11.1/go.mod h1:UkCTT2Sa8N7XNU0Z9Q+De6NvaxPlC7DGfSWDRowwXqY= +github.com/tdewolff/parse/v2 v2.5.28 h1:QziFVLe+bfFIwnCWAJzMrzwltQXPT21Evl9Z4x25D+U= +github.com/tdewolff/parse/v2 v2.5.28/go.mod h1:WzaJpRSbwq++EIQHYIRTpbYKNA3gn9it1Ik++q4zyho= github.com/tdewolff/test v1.0.6 h1:76mzYJQ83Op284kMT+63iCNCI7NEERsIN8dLM+RiKr4= github.com/tdewolff/test v1.0.6/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE= github.com/tklauser/go-sysconf v0.3.10 h1:IJ1AZGZRWbY8T5Vfk04D9WOA5WSejdflXxP03OUqALw= @@ -244,14 +244,14 @@ go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210314154223-e6e6c4f2bb5b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 h1:tkVvjkPTB7pnW3jnid7kNyAMPVWllTNOf/qKDze4p9o= -golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 h1:kUhD7nTDoI3fVd9G4ORWrbV5NY0liEs/Jg2pv5f+bBA= +golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220401154927-543a649e0bdd h1:zYlwaUHTmxuf6H7hwO2dgwqozQmH7zf4x+/qql4oVWc= -golang.org/x/net v0.0.0-20220401154927-543a649e0bdd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220412020605-290c469a71a5 h1:bRb386wvrE+oBNdF1d/Xh9mQrfQ4ecYhW5qJ5GvTGT4= +golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190130150945-aca44879d564/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -266,16 +266,16 @@ golang.org/x/sys v0.0.0-20201207223542-d4d67f95c62d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220330033206-e17cdc41300f h1:rlezHXNlxYWvBCzNses9Dlc7nGFaNMJeqLolcmQSSZY= -golang.org/x/sys v0.0.0-20220330033206-e17cdc41300f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad h1:ntjMns5wyP/fN65tdBD4g8J5w8n015+iIIs9rtjXkY0= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/time v0.0.0-20220224211638-0e9765cccd65 h1:M73Iuj3xbbb9Uk1DYhzydthsj6oOd6l9bpuFcNoUvTs= -golang.org/x/time v0.0.0-20220224211638-0e9765cccd65/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20220411224347-583f2d630306 h1:+gHMid33q6pen7kv9xvT+JRinntgeXO2AeZVd0AWD3w= +golang.org/x/time v0.0.0-20220411224347-583f2d630306/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=