release v12.2.0-beta1

PR: #1871.
This commit is contained in:
Gerasimos (Makis) Maropoulos 2022-04-13 02:46:52 +03:00 committed by GitHub
commit e98c21d1c5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
62 changed files with 3021 additions and 391 deletions

View File

@ -13,4 +13,4 @@ name = "go"
enabled = true
[analyzers.meta]
import_paths = ["github.com/kataras/iris"]
import_paths = ["github.com/kataras/iris/v12"]

View File

@ -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

View File

@ -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
View File

@ -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

View File

@ -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>

View File

@ -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)
@ -213,6 +214,7 @@
* [Ttemplates and Functions](i18n/template)
* [Pluralization and Variables](i18n/plurals)
* Authentication, Authorization & Bot Detection
* [Recommended: Auth package and Single-Sign-On](auth/auth) **NEW (GO 1.18 Generics required)**
* Basic Authentication
* [Basic](auth/basicauth/basic)
* [Load from a slice of Users](auth/basicauth/users_list)
@ -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)

View 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

View 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
View 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)
}
}

View 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
}

View 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
}

View 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>

View File

@ -0,0 +1 @@
<i>Iris Web Framework &copy; 2022</i>

View 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>

View File

@ -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()
}

View 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.
*/

View File

@ -0,0 +1 @@
Hello, world!

View File

@ -2,6 +2,7 @@ package main
import (
"context"
"log"
pb "github.com/kataras/iris/v12/_examples/mvc/grpc-compatible/helloworld"
@ -47,7 +48,9 @@ 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{
mvc.New(app).
Register(new(myService)).
Handle(ctrl, mvc.GRPC{
Server: grpcServer, // Required.
ServiceName: "helloworld.Greeter", // Required.
Strict: false,
@ -56,8 +59,21 @@ func newApp() *iris.Application {
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
}

View File

@ -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 == "" {

View 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=

View 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>

View 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
}

View 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
}

View 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
}

View 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>

View File

@ -0,0 +1 @@
<i>Iris Web Framework &copy; 2022</i>

View 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>

View File

@ -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"`
}

View File

@ -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))
}

View File

@ -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())

View File

@ -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
)

View File

@ -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=

View File

@ -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)

View File

@ -0,0 +1,9 @@
package database
type DB struct {
/* ... */
}
func Open(connString string) *DB {
return &DB{}
}

View File

@ -1,4 +1,5 @@
ServerName: My Project
Env: development
Host: 0.0.0.0
Port: 80
EnableCompression: true

View File

@ -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}
}

View File

@ -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))
}

View File

@ -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)
}

View File

@ -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).

View 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
}
```

View File

@ -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
View 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
View 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
View 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
View 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)
}

View File

@ -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{}),
}
}

View File

@ -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.
//

View File

@ -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.

View File

@ -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) {
func (ctx *Context) handleSpecialJSONResponseValue(v interface{}, options *JSON) (bool, int, error) {
if ctx.app.ConfigurationReadOnly().GetEnableProtoJSON() {
if m, ok := v.(proto.Message); ok {
result, err := options.Proto.Marshal(m)
protoJSON := ProtoMarshalOptions{}
if options != nil {
protoJSON = options.Proto
}
result, err := protoJSON.Marshal(m)
if err != nil {
return true, 0, err
}
n, err := w.Write(result)
n, err := ctx.writer.Write(result)
return true, n, err
}
}
if ctx.app.ConfigurationReadOnly().GetEnableEasyJSON() {
if easyObject, ok := v.(easyjson.Marshaler); ok {
jw := jwriter.Writer{NoEscapeHTML: !options.UnescapeHTML}
noEscapeHTML := false
if options != nil {
noEscapeHTML = !options.UnescapeHTML
}
jw := jwriter.Writer{NoEscapeHTML: noEscapeHTML}
easyObject.MarshalEasyJSON(&jw)
n, err := jw.DumpTo(w)
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,34 +3966,79 @@ 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{}
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)
)
// 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.
// HandleContextError completes the ErrorHandler interface.
func (h ErrorHandlerFunc) HandleContextError(ctx *Context, err error) {
h(ctx, err)
}
func (ctx *Context) handleContextError(err error) {
if errHandler := ctx.app.GetContextErrorHandler(); errHandler != nil {
errHandler.HandleContextError(ctx, err)
} else {
ctx.StatusCode(http.StatusInternalServerError)
}
// 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)
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 {
// 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
}
// as soon as possible, use the fast json marshaler with the http request context.
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
@ -3966,46 +4047,11 @@ func (ctx *Context) JSON(v interface{}, opts ...JSON) (n int, err error) {
return ctx.Write(result)
}
options := DefaultJSONOptions
if optsLength > 0 {
options = opts[0]
// Else if no options given neither optimizations are enabled, then safely read the already-initialized object.
options = &DefaultJSONOptions
}
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)
ctx.StatusCode(http.StatusInternalServerError)
return 0, err
}
return n, err
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
View 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
}
}
}
}

View File

@ -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
View File

@ -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
View File

@ -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
View File

@ -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
View File

@ -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

View File

@ -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

View 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)
}
}

View File

@ -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)
}

View File

@ -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
View 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
}