mirror of
https://github.com/kataras/iris.git
synced 2025-01-23 10:41:03 +01:00
commit
e98c21d1c5
|
@ -13,4 +13,4 @@ name = "go"
|
|||
enabled = true
|
||||
|
||||
[analyzers.meta]
|
||||
import_paths = ["github.com/kataras/iris"]
|
||||
import_paths = ["github.com/kataras/iris/v12"]
|
||||
|
|
|
@ -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
|
||||
|
|
11
HISTORY.md
11
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.
|
||||
|
||||
|
|
4
NOTICE
4
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
|
||||
|
|
20
README.md
20
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
|
||||
```
|
||||
|
||||
<details><summary>Install on existing project</summary>
|
||||
|
@ -291,25 +291,11 @@ $ cd myapp
|
|||
$ go get github.com/kataras/iris/v12@master
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details><summary>Install with a go.mod file</summary>
|
||||
|
||||
```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 .
|
||||
```
|
||||
|
||||
</details>
|
||||
|
|
|
@ -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)
|
||||
|
|
12
_examples/auth/auth/README.md
Normal file
12
_examples/auth/auth/README.md
Normal file
|
@ -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
|
36
_examples/auth/auth/auth.yml
Normal file
36
_examples/auth/auth/auth.yml
Normal file
|
@ -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=
|
135
_examples/auth/auth/main.go
Normal file
135
_examples/auth/auth/main.go
Normal file
|
@ -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)
|
||||
}
|
||||
}
|
33
_examples/auth/auth/user.go
Normal file
33
_examples/auth/auth/user.go
Normal file
|
@ -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
|
||||
}
|
100
_examples/auth/auth/user_provider.go
Normal file
100
_examples/auth/auth/user_provider.go
Normal file
|
@ -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
|
||||
}
|
30
_examples/auth/auth/views/layouts/main.html
Normal file
30
_examples/auth/auth/views/layouts/main.html
Normal file
|
@ -0,0 +1,30 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{ if .Title }}{{ .Title }}{{ else }}Default Main Title{{ end }}</title>
|
||||
</head>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
flex-direction: column;
|
||||
}
|
||||
main {
|
||||
display: block;
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
.container {
|
||||
max-width: 500px;
|
||||
margin: auto;
|
||||
}
|
||||
</style>
|
||||
<body>
|
||||
<div class="container">
|
||||
<main>{{ template "content" . }}</main>
|
||||
<footer style="position: fixed; bottom: 0; width: 100%;">{{ partial "partials/footer" .}}</footer>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
1
_examples/auth/auth/views/partials/footer.html
Normal file
1
_examples/auth/auth/views/partials/footer.html
Normal file
|
@ -0,0 +1 @@
|
|||
<i>Iris Web Framework © 2022</i>
|
9
_examples/auth/auth/views/signin.html
Normal file
9
_examples/auth/auth/views/signin.html
Normal file
|
@ -0,0 +1,9 @@
|
|||
<div class="user_signin">
|
||||
<form action="" method="post">
|
||||
<label for="username">Email:</label>
|
||||
<input name="username" type="email" />
|
||||
<label for="password">Password:</label>
|
||||
<input name="password" type="password" />
|
||||
<input type="submit" value="Sign in" />
|
||||
</form>
|
||||
</div>
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
57
_examples/file-server/webdav/main.go
Normal file
57
_examples/file-server/webdav/main.go
Normal file
|
@ -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.
|
||||
*/
|
0
_examples/file-server/webdav/newdir/.gitkeep
Normal file
0
_examples/file-server/webdav/newdir/.gitkeep
Normal file
1
_examples/file-server/webdav/test.txt
Normal file
1
_examples/file-server/webdav/test.txt
Normal file
|
@ -0,0 +1 @@
|
|||
Hello, world!
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 == "" {
|
||||
|
|
36
_examples/mvc/websocket-auth/auth.yml
Normal file
36
_examples/mvc/websocket-auth/auth.yml
Normal file
|
@ -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=
|
106
_examples/mvc/websocket-auth/browser/index.html
Normal file
106
_examples/mvc/websocket-auth/browser/index.html
Normal file
|
@ -0,0 +1,106 @@
|
|||
<html>
|
||||
|
||||
<head>
|
||||
<title>Online visitors MVC example</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, "San Francisco", "Helvetica Neue", "Noto", "Roboto", "Calibri Light", sans-serif;
|
||||
color: #212121;
|
||||
font-size: 1.0em;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 750px;
|
||||
margin: auto;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
#online_visitors {
|
||||
font-weight: bold;
|
||||
font-size: 18px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<span id="online_visitors">1 online visitor</span>
|
||||
</div>
|
||||
|
||||
<!-- the message's input -->
|
||||
<input id="input" type="text" />
|
||||
|
||||
<!-- when clicked then a websocket event will be sent to the server, at this example we registered the 'chat' -->
|
||||
<button id="sendBtn" disabled>Send</button>
|
||||
|
||||
<!-- the messages will be shown here -->
|
||||
<pre id="output"></pre>
|
||||
<!-- import the iris client-side library for browser from a CDN or locally.
|
||||
However, `neffos.(min.)js` is a NPM package too so alternatively,
|
||||
you can use it as dependency on your package.json and all nodejs-npm tooling become available:
|
||||
see the "browserify" example for more-->
|
||||
<script src="https://cdn.jsdelivr.net/npm/neffos.js@latest/dist/neffos.min.js"></script>
|
||||
<script type="text/javascript">
|
||||
const wsURL = "ws://localhost:8080/protected/ws"
|
||||
var outputTxt = document.getElementById("output");
|
||||
function addMessage(msg) {
|
||||
outputTxt.innerHTML += msg + "\n";
|
||||
}
|
||||
|
||||
async function runExample() {
|
||||
try {
|
||||
const conn = await neffos.dial(wsURL, {
|
||||
default: { // "default" namespace.
|
||||
_OnNamespaceConnected: function (nsConn, msg) {
|
||||
if (nsConn.conn.wasReconnected()) {
|
||||
addMessage("re-connected after " + nsConn.conn.reconnectTries.toString() + " trie(s)");
|
||||
}
|
||||
|
||||
let inputTxt = document.getElementById("input");
|
||||
let sendBtn = document.getElementById("sendBtn");
|
||||
|
||||
sendBtn.disabled = false;
|
||||
sendBtn.onclick = function () {
|
||||
const input = inputTxt.value;
|
||||
inputTxt.value = "";
|
||||
nsConn.emit("OnChat", input);
|
||||
addMessage("Me: " + input);
|
||||
};
|
||||
|
||||
addMessage("connected to namespace: " + msg.Namespace);
|
||||
},
|
||||
_OnNamespaceDisconnect: function (nsConn, msg) {
|
||||
addMessage("disconnected from namespace: " + msg.Namespace);
|
||||
},
|
||||
OnChat: function (nsConn, msg) { // "OnChat" event.
|
||||
console.log(msg);
|
||||
|
||||
addMessage(msg.Body);
|
||||
},
|
||||
OnVisit: function (nsConn, msg) {
|
||||
const newCount = Number(msg.Body); // or parseInt.
|
||||
console.log("visit websocket event with newCount of: ", newCount);
|
||||
|
||||
var text = "1 online visitor";
|
||||
if (newCount > 1) {
|
||||
text = newCount + " online visitors";
|
||||
}
|
||||
document.getElementById("online_visitors").innerHTML = text;
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
conn.connect("default");
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
}
|
||||
}
|
||||
|
||||
runExample();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
72
_examples/mvc/websocket-auth/main.go
Normal file
72
_examples/mvc/websocket-auth/main.go
Normal file
|
@ -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
|
||||
}
|
33
_examples/mvc/websocket-auth/user.go
Normal file
33
_examples/mvc/websocket-auth/user.go
Normal file
|
@ -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
|
||||
}
|
100
_examples/mvc/websocket-auth/user_provider.go
Normal file
100
_examples/mvc/websocket-auth/user_provider.go
Normal file
|
@ -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
|
||||
}
|
30
_examples/mvc/websocket-auth/views/layouts/main.html
Normal file
30
_examples/mvc/websocket-auth/views/layouts/main.html
Normal file
|
@ -0,0 +1,30 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{ if .Title }}{{ .Title }}{{ else }}Default Main Title{{ end }}</title>
|
||||
</head>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
flex-direction: column;
|
||||
}
|
||||
main {
|
||||
display: block;
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
.container {
|
||||
max-width: 500px;
|
||||
margin: auto;
|
||||
}
|
||||
</style>
|
||||
<body>
|
||||
<div class="container">
|
||||
<main>{{ template "content" . }}</main>
|
||||
<footer style="position: fixed; bottom: 0; width: 100%;">{{ partial "partials/footer" .}}</footer>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
1
_examples/mvc/websocket-auth/views/partials/footer.html
Normal file
1
_examples/mvc/websocket-auth/views/partials/footer.html
Normal file
|
@ -0,0 +1 @@
|
|||
<i>Iris Web Framework © 2022</i>
|
9
_examples/mvc/websocket-auth/views/signin.html
Normal file
9
_examples/mvc/websocket-auth/views/signin.html
Normal file
|
@ -0,0 +1,9 @@
|
|||
<div class="user_signin">
|
||||
<form action="" method="post">
|
||||
<label for="username">Email:</label>
|
||||
<input name="username" type="email" />
|
||||
<label for="password">Password:</label>
|
||||
<input name="password" type="password" />
|
||||
<input type="submit" value="Sign in" />
|
||||
</form>
|
||||
</div>
|
|
@ -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"`
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
47
_examples/project/go.sum
generated
47
_examples/project/go.sum
generated
|
@ -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=
|
||||
|
|
|
@ -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)
|
||||
|
|
9
_examples/project/pkg/database/database.go
Normal file
9
_examples/project/pkg/database/database.go
Normal file
|
@ -0,0 +1,9 @@
|
|||
package database
|
||||
|
||||
type DB struct {
|
||||
/* ... */
|
||||
}
|
||||
|
||||
func Open(connString string) *DB {
|
||||
return &DB{}
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
ServerName: My Project
|
||||
Env: development
|
||||
Host: 0.0.0.0
|
||||
Port: 80
|
||||
EnableCompression: true
|
||||
|
|
|
@ -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}
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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).
|
||||
|
|
81
_proposals/generic_handler.md
Normal file
81
_proposals/generic_handler.md
Normal file
|
@ -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
|
||||
}
|
||||
```
|
84
aliases.go
84
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",
|
||||
}
|
||||
)
|
||||
|
|
638
auth/auth.go
Normal file
638
auth/auth.go
Normal file
|
@ -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 <T> 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)
|
||||
}
|
||||
}
|
212
auth/configuration.go
Normal file
212
auth/configuration.go
Normal file
|
@ -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
|
||||
}
|
129
auth/provider.go
Normal file
129
auth/provider.go
Normal file
|
@ -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)
|
||||
}
|
62
auth/user.go
Normal file
62
auth/user.go
Normal file
|
@ -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)
|
||||
}
|
117
configuration.go
117
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{}),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
//
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
23
context/context_go118.go
Normal file
23
context/context_go118.go
Normal file
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
4
doc.go
4
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
|
||||
|
||||
|
|
31
go.mod
31
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
|
||||
|
|
64
go.sum
generated
64
go.sum
generated
|
@ -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=
|
||||
|
|
88
iris.go
88
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
|
||||
|
|
|
@ -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
|
||||
|
|
79
middleware/modrevision/modrevision.go
Normal file
79
middleware/modrevision/modrevision.go
Normal file
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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 = "<empty>"
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
|
246
x/pagination/pagination.go
Normal file
246
x/pagination/pagination.go
Normal file
|
@ -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
|
||||
}
|
Loading…
Reference in New Issue
Block a user