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/.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 113350c1..237cfa66 100644
--- a/HISTORY.md
+++ b/HISTORY.md
@@ -28,6 +28,15 @@ 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).
+- 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.~ 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).
- Add a new [x/sqlx](/x/sqlx/) sub-package ([example](_examples/database/sqlx/main.go)).
@@ -64,7 +73,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/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/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/_examples/README.md b/_examples/README.md
index 32be6859..8a7477e1 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,8 @@
* [Basic](i18n/basic)
* [Ttemplates and Functions](i18n/template)
* [Pluralization and Variables](i18n/plurals)
-* Authentication, Authorization & Bot Detection
+* 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)
@@ -277,6 +279,7 @@
* [Authenticated Controller](mvc/authenticated-controller/main.go)
* [Versioned Controller](mvc/versioned-controller/main.go)
* [Websocket Controller](mvc/websocket)
+ * [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/auth/README.md b/_examples/auth/auth/README.md
new file mode 100644
index 00000000..3f91efeb
--- /dev/null
+++ b/_examples/auth/auth/README.md
@@ -0,0 +1,12 @@
+# Auth Package (+ 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/auth/auth.yml b/_examples/auth/auth/auth.yml
new file mode 100644
index 00000000..9cfed272
--- /dev/null
+++ b/_examples/auth/auth/auth.yml
@@ -0,0 +1,36 @@
+Headers: # required.
+ - "Authorization"
+ - "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:
+ - ID: IRIS_AUTH_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_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.
+ 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/auth/main.go b/_examples/auth/auth/main.go
new file mode 100644
index 00000000..c05a4133
--- /dev/null
+++ b/_examples/auth/auth/main.go
@@ -0,0 +1,135 @@
+//go:build go1.18
+
+package main
+
+import (
+ "fmt"
+
+ "github.com/kataras/iris/v12"
+ "github.com/kataras/iris/v12/auth"
+)
+
+func allowRole(role AccessRole) auth.VerifyUserFunc[User] {
+ return func(u User) error {
+ if !u.Role.Allow(role) {
+ return fmt.Errorf("invalid role")
+ }
+
+ return nil
+ }
+}
+
+const configFilename = "./auth.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 auth instance:
+ s := auth.MustLoad[User]("./auth.yml")
+ // Bind a configuration from file:
+ var c auth.Configuration
+ c.BindFile("./auth.yml")
+ s, err := auth.New[User](c)
+ // OR create new programmatically configuration:
+ config := auth.Configuration{
+ ...fields
+ }
+ s, err := auth.New[User](config)
+ // OR generate a new configuration:
+ 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 := auth.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.
+ authConfig, err := auth.LoadConfiguration(configFilename)
+ if err != nil {
+ panic(err)
+ }
+
+ // 2. Initialize a new auth instance for "User" claims (generics: go1.18 +).
+ s, err := auth.New[User](authConfig)
+ 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(auth.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 Auth (Test)",
+ Addr: ":8080",
+ Hostname: "YOUR_DOMAIN",
+ },
+ {
+ Name: "Iris Auth (Test Subdomain)",
+ Addr: ":8080",
+ Hostname: "owner.YOUR_DOMAIN",
+ },
+ },
+ },
+ })*/
+}
+
+func renderSigninForm(ctx iris.Context) {
+ ctx.View("signin", iris.Map{"Title": "Signin Page"})
+}
+
+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 *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/auth/user.go b/_examples/auth/auth/user.go
new file mode 100644
index 00000000..a2e93017
--- /dev/null
+++ b/_examples/auth/auth/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/auth/user_provider.go b/_examples/auth/auth/user_provider.go
new file mode 100644
index 00000000..aedbc10a
--- /dev/null
+++ b/_examples/auth/auth/user_provider.go
@@ -0,0 +1,100 @@
+//go:build go1.18
+
+package main
+
+import (
+ "context"
+ "fmt"
+ "sync"
+ "time"
+
+ "github.com/kataras/iris/v12/auth"
+)
+
+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 auth.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 auth.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/auth/views/layouts/main.html b/_examples/auth/auth/views/layouts/main.html
new file mode 100644
index 00000000..c6efd56d
--- /dev/null
+++ b/_examples/auth/auth/views/layouts/main.html
@@ -0,0 +1,30 @@
+
+
+
+
+
+ {{ if .Title }}{{ .Title }}{{ else }}Default Main Title{{ end }}
+
+
+
+
+ {{ template "content" . }}
+
+
+
+
\ No newline at end of file
diff --git a/_examples/auth/auth/views/partials/footer.html b/_examples/auth/auth/views/partials/footer.html
new file mode 100644
index 00000000..69b8f9b8
--- /dev/null
+++ b/_examples/auth/auth/views/partials/footer.html
@@ -0,0 +1 @@
+Iris Web Framework © 2022
\ No newline at end of file
diff --git a/_examples/auth/auth/views/signin.html b/_examples/auth/auth/views/signin.html
new file mode 100644
index 00000000..57f7514c
--- /dev/null
+++ b/_examples/auth/auth/views/signin.html
@@ -0,0 +1,9 @@
+
+
+
\ No newline at end of file
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/file-server/webdav/main.go b/_examples/file-server/webdav/main.go
new file mode 100644
index 00000000..be981a8e
--- /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.ErrURLQuerySemicolon),
+ 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/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/mvc/websocket-auth/auth.yml b/_examples/mvc/websocket-auth/auth.yml
new file mode 100644
index 00000000..9cfed272
--- /dev/null
+++ b/_examples/mvc/websocket-auth/auth.yml
@@ -0,0 +1,36 @@
+Headers: # required.
+ - "Authorization"
+ - "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:
+ - ID: IRIS_AUTH_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_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.
+ 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-auth/browser/index.html b/_examples/mvc/websocket-auth/browser/index.html
new file mode 100644
index 00000000..09bd4498
--- /dev/null
+++ b/_examples/mvc/websocket-auth/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-auth/main.go b/_examples/mvc/websocket-auth/main.go
new file mode 100644
index 00000000..89a52d93
--- /dev/null
+++ b/_examples/mvc/websocket-auth/main.go
@@ -0,0 +1,72 @@
+//go:build go1.18
+
+package main
+
+import (
+ "fmt"
+
+ "github.com/kataras/iris/v12"
+ "github.com/kataras/iris/v12/auth"
+ "github.com/kataras/iris/v12/mvc"
+ "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 := auth.MustLoad[User]("./auth.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 := auth.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-auth/user.go b/_examples/mvc/websocket-auth/user.go
new file mode 100644
index 00000000..a2e93017
--- /dev/null
+++ b/_examples/mvc/websocket-auth/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-auth/user_provider.go b/_examples/mvc/websocket-auth/user_provider.go
new file mode 100644
index 00000000..16fbd7ce
--- /dev/null
+++ b/_examples/mvc/websocket-auth/user_provider.go
@@ -0,0 +1,100 @@
+//go:build go1.18
+
+package main
+
+import (
+ "context"
+ "fmt"
+ "sync"
+ "time"
+
+ "github.com/kataras/iris/v12/auth"
+)
+
+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 auth.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 auth.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-auth/views/layouts/main.html b/_examples/mvc/websocket-auth/views/layouts/main.html
new file mode 100644
index 00000000..c6efd56d
--- /dev/null
+++ b/_examples/mvc/websocket-auth/views/layouts/main.html
@@ -0,0 +1,30 @@
+
+
+
+
+
+ {{ if .Title }}{{ .Title }}{{ else }}Default Main Title{{ end }}
+
+
+
+
+ {{ template "content" . }}
+
+
+
+
\ No newline at end of file
diff --git a/_examples/mvc/websocket-auth/views/partials/footer.html b/_examples/mvc/websocket-auth/views/partials/footer.html
new file mode 100644
index 00000000..69b8f9b8
--- /dev/null
+++ b/_examples/mvc/websocket-auth/views/partials/footer.html
@@ -0,0 +1 @@
+Iris Web Framework © 2022
\ No newline at end of file
diff --git a/_examples/mvc/websocket-auth/views/signin.html b/_examples/mvc/websocket-auth/views/signin.html
new file mode 100644
index 00000000..57f7514c
--- /dev/null
+++ b/_examples/mvc/websocket-auth/views/signin.html
@@ -0,0 +1,9 @@
+
+
+
\ No newline at end of file
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/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/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/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/_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/_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/_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/_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).
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/aliases.go b/aliases.go
index da362586..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"
@@ -14,6 +16,16 @@ import (
"github.com/kataras/iris/v12/view"
)
+var (
+ // 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 of the program's build.
+ // Available at go version 1.18+
+ BuildTime = context.BuildTime
+)
+
// SameSite attributes.
const (
SameSiteDefaultMode = http.SameSiteDefaultMode
@@ -318,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
@@ -660,8 +701,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/auth/auth.go b/auth/auth.go
new file mode 100644
index 00000000..93c74d09
--- /dev/null
+++ b/auth/auth.go
@@ -0,0 +1,638 @@
+//go:build go1.18
+
+package auth
+
+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 (
+ // 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
+ // 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
+ // 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
+ // True if KIDRefresh on config.Keys.
+ refreshEnabled bool
+ }
+
+ // 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)
+ }
+
+ 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)
+ }
+
+ 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 {
+ return nil, err
+ }
+ _, refreshEnabled := keys[KIDRefresh]
+
+ s := &Auth[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
+}
+
+// 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 {
+ 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
+}
+
+// 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.
+ 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
+}
+
+// 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
+
+ // 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("auth: signin: %w", err)
+ }
+ // keep searching.
+ continue
+ }
+
+ // found.
+ t = v
+ break
+ }
+ } else {
+ 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("auth: signin: %w", err)
+ }
+
+ return accessToken, refreshToken, nil
+}
+
+func (s *Auth[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
+}
+
+// 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)
+
+ 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)
+}
+
+// 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 {
+ 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
+}
+
+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.
+ 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
+}
+
+// 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)
+
+ if accessToken == "" { // if empty, fire 401.
+ s.errorHandler.Unauthenticated(ctx, jwt.ErrMissing)
+ return
+ }
+
+ t, claims, err := s.Verify(ctx, []byte(accessToken), verifyFuncs...)
+ if err != nil {
+ s.errorHandler.Unauthenticated(ctx, err)
+ return
+ }
+
+ ctx.SetUser(t)
+
+ // store the user to the request.
+ ctx.Values().Set(accessTokenContextKey, accessToken)
+ ctx.Values().Set(standardClaimsContextKey, claims)
+ ctx.Values().Set(userContextKey, t)
+
+ ctx.Next()
+ }
+}
+
+func (s *Auth[T]) extractAccessToken(ctx *context.Context) string {
+ // first try from authorization: bearer header.
+ accessToken := s.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
+}
+
+// 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")
+ }
+
+ t, _, err := s.verify(ctx, refreshToken)
+ if err != nil {
+ 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("auth: refresh: %w", err)
+ }
+
+ 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)
+ 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)
+}
+
+// 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 {
+ return fmt.Errorf("auth: 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
+}
+
+// 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)
+}
+
+func (s *Auth[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(standardClaimsContextKey)
+ ctx.Values().Remove(userContextKey)
+}
+
+func (s *Auth[T]) extractTokenFromHeader(ctx *context.Context) string {
+ for _, headerKey := range s.config.Headers {
+ 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 *Auth[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{
+ Name: cookieName,
+ Value: url.QueryEscape(accessToken),
+ Path: "/",
+ HttpOnly: true,
+ Secure: s.config.Cookie.Secure || ctx.IsSSL(),
+ Domain: ctx.Domain(),
+ SameSite: http.SameSiteLaxMode,
+ Expires: time.Now().Add(maxAge),
+ MaxAge: int(maxAge.Seconds()),
+ }
+
+ ctx.SetCookie(cookie, context.CookieEncoding(s.securecookie), context.CookieAllowReclaim())
+ }
+}
+
+func (s *Auth[T]) tryRemoveCookie(ctx *context.Context) {
+ if cookieName := s.config.Cookie.Name; cookieName != "" {
+ ctx.RemoveCookie(cookieName)
+ }
+}
diff --git a/auth/configuration.go b/auth/configuration.go
new file mode 100644
index 00000000..fb3ea57e
--- /dev/null
+++ b/auth/configuration.go
@@ -0,0 +1,212 @@
+//go:build go1.18
+
+package auth
+
+import (
+ "encoding/json"
+ "errors"
+ "fmt"
+ "os"
+ "path/filepath"
+ "time"
+
+ "github.com/gorilla/securecookie"
+ "github.com/kataras/jwt"
+ "gopkg.in/yaml.v3"
+)
+
+const (
+ // 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"`
+ // 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 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"`
+ // 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("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("auth: configuration: %w", err)
+ }
+
+ if _, ok := keys[KIDAccess]; !ok {
+ 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("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,
+// see BindFile method and LoadConfiguration/MustLoadConfiguration package-level functions.
+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{
+ Headers: []string{
+ "Authorization",
+ "X-Authorization",
+ },
+ Cookie: CookieConfiguration{
+ Name: "iris_auth_cookie",
+ Secure: false,
+ 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
+}
+
+// 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":
+ 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)
+ }
+
+}
+
+// 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)
+ }
+
+ return
+}
+
+// MustLoadConfiguration same as LoadConfiguration package-level function
+// but it panics on error.
+func MustLoadConfiguration(filename string) Configuration {
+ c, err := LoadConfiguration(filename)
+ if err != nil {
+ panic(err)
+ }
+
+ 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
new file mode 100644
index 00000000..9261e60f
--- /dev/null
+++ b/auth/provider.go
@@ -0,0 +1,129 @@
+//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 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.
+ //
+ // 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
+ // 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.
+ //
+ // 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
+
+ // 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.
+ //
+ // 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
+}
+
+// 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
+}
+
+// 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.
+//
+// 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)
+}
+
+// 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
new file mode 100644
index 00000000..9958077d
--- /dev/null
+++ b/auth/user.go
@@ -0,0 +1,62 @@
+//go:build go1.18
+
+package auth
+
+import (
+ "github.com/kataras/iris/v12/context"
+
+ "github.com/kataras/jwt"
+)
+
+type (
+ // StandardClaims is an alias of jwt.Claims, it holds the standard JWT claims.
+ 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.
+ //
+ // 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"
+
+// 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 {
+ return c
+ }
+ }
+
+ return StandardClaims{}
+}
+
+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 {
+ return t
+ }
+ }
+
+ var empty 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)
+}
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 ca479f36..fe7fa6f3 100644
--- a/context/context.go
+++ b/context/context.go
@@ -45,6 +45,15 @@ import (
"gopkg.in/yaml.v3"
)
+var (
+ // 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 of the program's build.
+ // Available at go version 1.18+
+ BuildTime string
+)
+
type (
// BodyDecoder is an interface which any struct can implement in order to customize the decode action
// from ReadJSON and ReadXML
@@ -3037,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)
}
@@ -3748,21 +3760,21 @@ 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
+ Proto ProtoMarshalOptions `yaml:"ProtoMarshalOptions"`
}
+// 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 {
return j.StreamingJSON == DefaultJSONOptions.StreamingJSON &&
@@ -3774,15 +3786,6 @@ func (j *JSON) IsDefault() bool {
j.Proto == DefaultJSONOptions.Proto
}
-// 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.
type JSONP struct {
// content-specific
@@ -3821,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 (
@@ -3854,9 +3889,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 {
@@ -3870,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)
}
@@ -3910,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))
@@ -3930,82 +3966,92 @@ func WriteJSON(writer io.Writer, v interface{}, options JSON, shouldOptimize boo
}
// 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))
}
-// DefaultJSONOptions is the optional settings that are being used
-// inside `ctx.JSON`.
-var DefaultJSONOptions = JSON{}
-
-// 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) {
- ctx.ContentType(ContentJSONHeaderValue)
- shouldOptimize := ctx.shouldOptimize()
-
- optsLength := len(opts)
-
- 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 {
- 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)
+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)
+)
- options := DefaultJSONOptions
- if optsLength > 0 {
- options = opts[0]
- }
+// HandleContextError completes the ErrorHandler interface.
+func (h ErrorHandlerFunc) HandleContextError(ctx *Context, err error) {
+ h(ctx, err)
+}
- if options.StreamingJSON {
- 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 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
- }
-
- n, err = WriteJSON(ctx.writer, v, options, shouldOptimize)
- if err != nil {
- ctx.app.Logger().Debugf("JSON: %v", err)
+func (ctx *Context) handleContextError(err error) {
+ if errHandler := ctx.app.GetContextErrorHandler(); errHandler != nil {
+ errHandler.HandleContextError(ctx, err)
+ } else {
ctx.StatusCode(http.StatusInternalServerError)
- return 0, err
}
- return n, err
+ // keep the error non nil so the caller has control over further actions.
+}
+
+// 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) {
+ 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{}, 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()
+ if options == nil {
+ if shouldOptimize {
+ // 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)
+ }
+
+ // Else if no options given neither optimizations are enabled, then safely read the already-initialized object.
+ options = &DefaultJSONOptions
+ }
+
+ return WriteJSON(ctx, ctx.writer, v, options, shouldOptimize)
}
var finishCallbackB = []byte(");")
@@ -4056,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 {
@@ -4154,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.
//
@@ -4221,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)
}
@@ -4234,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
}
// +-----------------------------------------------------------------------+
@@ -5314,7 +5383,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 +5401,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/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
+ }
+ }
+ }
+}
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/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/go.mod b/go.mod
index a1d99da5..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,16 +15,17 @@ 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.20220412154129-171d97575378
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
@@ -35,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/tdewolff/minify/v2 v2.10.0
+ github.com/shirou/gopsutil/v3 v3.22.3
+ 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-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-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
- google.golang.org/protobuf v1.27.1
+ 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
)
@@ -68,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
@@ -91,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/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/stretchr/testify v1.7.1 // 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
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 379cb4aa..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.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.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,10 +81,12 @@ 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=
+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=
@@ -177,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=
@@ -199,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/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/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.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.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=
@@ -241,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-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-20220225172249-27dd8689420f h1:oA4XRj0qtSt8Yo1Zms0CUlsT3KG69V2UGQWPBxujDmc=
-golang.org/x/net v0.0.0-20220225172249-27dd8689420f/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=
@@ -262,18 +265,17 @@ 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-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=
@@ -281,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=
diff --git a/iris.go b/iris.go
index 0405d763..bcd23b68 100644
--- a/iris.go
+++ b/iris.go
@@ -1,6 +1,7 @@
package iris
import (
+ "bytes"
stdContext "context"
"errors"
"fmt"
@@ -38,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 (
@@ -59,6 +60,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 +432,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.
@@ -451,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).
@@ -463,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 == "" {
@@ -889,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
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..198339c8
--- /dev/null
+++ b/middleware/modrevision/modrevision.go
@@ -0,0 +1,79 @@
+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
+}
+
+// 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 {
+ buildTime, buildRevision := context.BuildTime, context.BuildRevision
+ if opts.UnixTime {
+ 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, buildTime); err == nil {
+ buildTime = t.In(opts.TimeLocation).String()
+ }
+ }
+
+ var buildInfo string
+ if buildInfo = opts.ServerName; buildInfo != "" {
+ if env := opts.Env; env != "" {
+ buildInfo += fmt.Sprintf(" (%s)", env)
+ }
+ }
+
+ 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 {
+ contents = []byte("OK")
+ }
+
+ return func(ctx *context.Context) {
+ ctx.Write(contents)
+ }
+}
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..2ef2438a 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")
@@ -247,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 = ""
}
@@ -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.
@@ -311,6 +312,6 @@ func fail(ctx iris.Context, codeName ErrorCodeName, msg, details string, validat
Validation: validationErrors,
}
- // ctx.SetErr(err)
+ // ctx.SetErr(&err)
ctx.StopWithJSON(errorCode.Status, err)
}
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