diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 9eaca0bb..8e75794c 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -17,7 +17,7 @@ jobs:
strategy:
matrix:
- go_version: [1.18.x]
+ go_version: [1.19.x]
steps:
- name: Set up Go 1.x
diff --git a/HISTORY.md b/HISTORY.md
index 65df6d2e..05cee57e 100644
--- a/HISTORY.md
+++ b/HISTORY.md
@@ -28,6 +28,9 @@ The codebase for Dependency Injection, Internationalization and localization and
## Fixes and Improvements
+- Enable setting a custom "go-redis" client through `SetClient` go redis driver method or `Client` struct field on sessions/database/redis driver as requested at [chat](https://chat.iris-go.com).
+- Ignore `"csrf.token"` form data key when missing on `ctx.ReadForm` by default as requested at [#1941](https://github.com/kataras/iris/issues/1941).
+
- Fix [CVE-2020-5398](https://github.com/advisories/GHSA-8wx2-9q48-vm9r).
- New `{x:weekday}` path parameter type, example code:
diff --git a/README.md b/README.md
index a0d5b016..1f1fc2b2 100644
--- a/README.md
+++ b/README.md
@@ -246,6 +246,7 @@ With your help, we can improve Open Source web development for everyone!
+
@@ -337,6 +338,7 @@ With your help, we can improve Open Source web development for everyone!
+
@@ -368,6 +370,7 @@ With your help, we can improve Open Source web development for everyone!
+
@@ -380,6 +383,7 @@ With your help, we can improve Open Source web development for everyone!
+
@@ -393,6 +397,7 @@ With your help, we can improve Open Source web development for everyone!
+
@@ -470,7 +475,7 @@ $ go get github.com/kataras/iris/v12@master
**Run**
```sh
-$ go mod tidy -compat=1.18
+$ go mod tidy -compat=1.19
$ go run .
```
diff --git a/README_FA.md b/README_FA.md
index 89c1f3a8..a5df3f46 100644
--- a/README_FA.md
+++ b/README_FA.md
@@ -261,7 +261,7 @@ $ go get github.com/kataras/iris/v12@master
```txt
module myapp
-go 1.18
+go 1.19
require github.com/kataras/iris/v12 master
```
diff --git a/_examples/README.md b/_examples/README.md
index 4dbe6f81..55e5ddda 100644
--- a/_examples/README.md
+++ b/_examples/README.md
@@ -55,6 +55,7 @@
* [Not Found - Intelligence](routing/intelligence/main.go)
* [Not Found - Suggest Closest Paths](routing/intelligence/manual/main.go)
* [Dynamic Path](routing/dynamic-path/main.go)
+ * [At-username](routing/dynamic-path/at-username/main.go)
* [Root Wildcard](routing/dynamic-path/root-wildcard/main.go)
* [Implement a Parameter Type](routing/macros/main.go)
* [Same Path Pattern but Func](routing/dynamic-path/same-pattern-different-func/main.go)
diff --git a/_examples/routing/dynamic-path/at-username/main.go b/_examples/routing/dynamic-path/at-username/main.go
new file mode 100644
index 00000000..e6496d01
--- /dev/null
+++ b/_examples/routing/dynamic-path/at-username/main.go
@@ -0,0 +1,30 @@
+package main
+
+import "github.com/kataras/iris/v12"
+
+func main() {
+ app := iris.New()
+
+ app.Get("/", func(ctx iris.Context) {
+ ctx.Writef("Hello %s", "world")
+ })
+
+ // This is an Iris-only feature across all web frameworks
+ // in every programming language for years.
+ // Dynamic Route Path Parameters Functions.
+ // Set min length characters to 2.
+ // Prefix of the username is '@'
+ // Otherwise 404.
+ //
+ // You can also use the regexp(...) function for more advanced expressions.
+ app.Get("/{username:string min(2) prefix(@)}", func(ctx iris.Context) {
+ username := ctx.Params().Get("username")[1:]
+ ctx.Writef("Username is %s", username)
+ })
+
+ // http://localhost:8080 -> FOUND (Hello world)
+ // http://localhost:8080/other -> NOT FOUND
+ // http://localhost:8080/@ -> NOT FOUND
+ // http://localhost:8080/@kataras -> FOUND (username is kataras)
+ app.Listen(":8080")
+}
diff --git a/_examples/sessions/database/redis/main.go b/_examples/sessions/database/redis/main.go
index 81a15c12..e7f7cea5 100644
--- a/_examples/sessions/database/redis/main.go
+++ b/_examples/sessions/database/redis/main.go
@@ -30,11 +30,16 @@ func main() {
Password: "",
Database: "",
Prefix: "myapp-",
- Driver: redis.GoRedis(), // defaults.
+ Driver: redis.GoRedis(), // defaults to this driver.
+ // To set a custom, existing go-redis client, use the "SetClient" method:
+ // Driver: redis.GoRedis().SetClient(customGoRedisClient)
})
// Optionally configure the underline driver:
// driver := redis.GoRedis()
+ // // To set a custom client:
+ // driver.SetClient(customGoRedisClient)
+ // OR:
// driver.ClientOptions = redis.Options{...}
// driver.ClusterOptions = redis.ClusterOptions{...}
// redis.New(redis.Config{Driver: driver, ...})
diff --git a/context/context.go b/context/context.go
index 900785fc..2882ae3b 100644
--- a/context/context.go
+++ b/context/context.go
@@ -1012,6 +1012,10 @@ var GetDomain = func(hostport string) string {
// loopback.
return "localhost"
default:
+ if net.ParseIP(host) != nil { // if it's an IP, see #1945.
+ return host
+ }
+
if domain, err := publicsuffix.EffectiveTLDPlusOne(host); err == nil {
host = domain
}
@@ -2575,8 +2579,12 @@ func (ctx *Context) RecordRequestBody(b bool) {
// IsRecordingBody reports whether the request body can be readen multiple times.
func (ctx *Context) IsRecordingBody() bool {
- return ctx.values.GetBoolDefault(disableRequestBodyConsumptionContextKey,
- ctx.app.ConfigurationReadOnly().GetDisableBodyConsumptionOnUnmarshal())
+ if ctx.app.ConfigurationReadOnly().GetDisableBodyConsumptionOnUnmarshal() {
+ return true
+ }
+
+ value, _ := ctx.values.GetBool(disableRequestBodyConsumptionContextKey)
+ return value
}
// GetBody reads and returns the request body.
@@ -2672,7 +2680,20 @@ type JSONReader struct { // Note(@kataras): struct instead of optional funcs to
}
var ReadJSON = func(ctx *Context, outPtr interface{}, opts ...JSONReader) error {
- decoder := json.NewDecoder(ctx.request.Body)
+ var body io.Reader
+
+ if ctx.IsRecordingBody() {
+ data, err := io.ReadAll(ctx.request.Body)
+ if err != nil {
+ return err
+ }
+ setBody(ctx.request, data)
+ body = bytes.NewReader(data)
+ } else {
+ body = ctx.request.Body
+ }
+
+ decoder := json.NewDecoder(body)
// decoder := gojson.NewDecoder(ctx.Request().Body)
if len(opts) > 0 {
options := opts[0]
@@ -2778,6 +2799,24 @@ var (
// A shortcut for the `schema#IsErrPath`.
IsErrPath = schema.IsErrPath
+ // IsErrPathCRSFToken reports whether the given "err" is caused
+ // by unknown key error on "csrf.token". See `context#ReadForm` for more.
+ IsErrPathCRSFToken = func(err error) bool {
+ if err == nil || CSRFTokenFormKey == "" {
+ return false
+ }
+
+ if m, ok := err.(schema.MultiError); ok {
+ if csrfErr, hasCSRFToken := m[CSRFTokenFormKey]; hasCSRFToken {
+ _, is := csrfErr.(schema.UnknownKeyError)
+ return is
+
+ }
+ }
+
+ return false
+ }
+
// ErrEmptyForm is returned by
// - `context#ReadForm`
// - `context#ReadQuery`
@@ -2820,6 +2859,11 @@ var (
}
)
+// CSRFTokenFormKey the CSRF token key of the form data.
+//
+// See ReadForm method for more.
+const CSRFTokenFormKey = "csrf.token"
+
// ReadForm binds the request body of a form to the "formObject".
// It supports any kind of type, including custom structs.
// It will return nothing if request data are empty.
@@ -2831,6 +2875,9 @@ var (
// If a client sent an unknown field, this method will return an error,
// in order to ignore that error use the `err != nil && !iris.IsErrPath(err)`.
//
+// As of 15 Aug 2022, ReadForm does not return an error over unknown CSRF token form key,
+// to change this behavior globally, set the `context.CSRFTokenFormKey` to an empty value.
+//
// Example: https://github.com/kataras/iris/blob/master/_examples/request-body/read-form/main.go
func (ctx *Context) ReadForm(formObject interface{}) error {
values := ctx.FormValues()
@@ -2842,7 +2889,7 @@ func (ctx *Context) ReadForm(formObject interface{}) error {
}
err := schema.DecodeForm(values, formObject)
- if err != nil {
+ if err != nil && !IsErrPathCRSFToken(err) {
return err
}
diff --git a/core/errgroup/errgroup.go b/core/errgroup/errgroup.go
index c22d447a..ba8c4dc0 100644
--- a/core/errgroup/errgroup.go
+++ b/core/errgroup/errgroup.go
@@ -147,7 +147,7 @@ func (e *Error) As(target interface{}) bool {
}
}
- return errors.As(e.Err, &te.Err)
+ return errors.As(te.Err, &e)
}
return ok
diff --git a/core/errgroup/errgroup_test.go b/core/errgroup/errgroup_test.go
index f01fbfff..bb82a798 100644
--- a/core/errgroup/errgroup_test.go
+++ b/core/errgroup/errgroup_test.go
@@ -32,13 +32,22 @@ func TestErrorIs(t *testing.T) {
}
}
+// errorString is a trivial implementation of error.
+type errorString struct {
+ s string
+}
+
+func (e *errorString) Error() string {
+ return e.s
+}
+
func TestErrorAs(t *testing.T) {
- testErr := errors.New("as")
+ testErr := &errorString{"as"}
err := &Error{Err: testErr}
if expected, got := true, errors.As(err, &testErr); expected != got {
t.Fatalf("[testErr as err] expected %v but got %v", expected, got)
}
- if expected, got := true, errors.As(testErr, &err); expected != got {
+ if expected, got := false, errors.As(testErr, &err); expected != got /* errorString does not implemeny As, so the std/default functionality will be applied */ {
t.Fatalf("[err as testErr] expected %v but got %v", expected, got)
}
}
diff --git a/go.mod b/go.mod
index faf4da1d..21e959f7 100644
--- a/go.mod
+++ b/go.mod
@@ -1,13 +1,13 @@
module github.com/kataras/iris/v12
-go 1.18
+go 1.19
// retract v12.1.8 // please update to @master
require (
github.com/BurntSushi/toml v1.2.0
github.com/CloudyKit/jet/v6 v6.1.0
- github.com/Shopify/goreferrer v0.0.0-20210630161223-536fa16abd6f
+ github.com/Shopify/goreferrer v0.0.0-20220729165902-8cddb4f5de06
github.com/andybalholm/brotli v1.0.4
github.com/blang/semver/v4 v4.0.0
github.com/dgraph-io/badger/v2 v2.2007.4
@@ -40,11 +40,11 @@ require (
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-20220507011949-2cf3adece122
- golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4
- golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a
+ golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa
+ golang.org/x/net v0.0.0-20220812174116-3211cb980234
+ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab
golang.org/x/text v0.3.7
- golang.org/x/time v0.0.0-20220411224347-583f2d630306
+ golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9
google.golang.org/protobuf v1.28.1
gopkg.in/ini.v1 v1.67.0
gopkg.in/yaml.v3 v3.0.1
@@ -81,7 +81,7 @@ require (
github.com/minio/highwayhash v1.0.2 // indirect
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
- github.com/nats-io/jwt/v2 v2.2.0 // indirect
+ github.com/nats-io/jwt/v2 v2.3.0 // indirect
github.com/nats-io/nats.go v1.16.0 // indirect
github.com/nats-io/nkeys v0.3.0 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
diff --git a/go.sum b/go.sum
index 4fac3f0e..3d620f7d 100644
--- a/go.sum
+++ b/go.sum
@@ -9,8 +9,8 @@ github.com/Joker/hpp v1.0.0 h1:65+iuJYdRXv/XyN62C1uEmmOx3432rNG/rKlX6V7Kkc=
github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY=
github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
-github.com/Shopify/goreferrer v0.0.0-20210630161223-536fa16abd6f h1:XeOBnoBP7K19tMBEKeUo1NOxOO+h5FFi2HGzQvvkb44=
-github.com/Shopify/goreferrer v0.0.0-20210630161223-536fa16abd6f/go.mod h1:a1uqRtAwp2Xwc6WNPJEufxJ7fx3npB4UV/JOLmbu5I0=
+github.com/Shopify/goreferrer v0.0.0-20220729165902-8cddb4f5de06 h1:KkH3I3sJuOLP3TjA/dfr4NAY8bghDwnXiU7cTKxQqo0=
+github.com/Shopify/goreferrer v0.0.0-20220729165902-8cddb4f5de06/go.mod h1:7erjKLwalezA0k99cWs5L11HWOAPNjdUZ6RxH1BXbbM=
github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
@@ -150,8 +150,8 @@ github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OH
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
-github.com/nats-io/jwt/v2 v2.2.0 h1:Yg/4WFK6vsqMudRg91eBb7Dh6XeVcDMPHycDE8CfltE=
-github.com/nats-io/jwt/v2 v2.2.0/go.mod h1:0tqz9Hlu6bCBFLWAASKhE5vUA4c24L9KPUUgvwumE/k=
+github.com/nats-io/jwt/v2 v2.3.0 h1:z2mA1a7tIf5ShggOFlR1oBPgd6hGqcDYsISxZByUzdI=
+github.com/nats-io/jwt/v2 v2.3.0/go.mod h1:0tqz9Hlu6bCBFLWAASKhE5vUA4c24L9KPUUgvwumE/k=
github.com/nats-io/nats-server/v2 v2.8.4 h1:0jQzze1T9mECg8YZEl8+WYUXb9JKluJfCBriPUtluB4=
github.com/nats-io/nats.go v1.16.0 h1:zvLE7fGBQYW6MWaFaRdsgm9qT39PJDQoju+DS8KsO1g=
github.com/nats-io/nats.go v1.16.0/go.mod h1:BPko4oXsySz4aSWeFgOHLZs3G4Jq4ZAyE6/zMCxRT6w=
@@ -245,13 +245,13 @@ 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-20220507011949-2cf3adece122 h1:NvGWuYG8dkDHFSKksI1P9faiVJ9rayE6l0+ouWVIDs8=
-golang.org/x/crypto v0.0.0-20220507011949-2cf3adece122/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa h1:zuSxTR4o9y82ebqCUJYNGJbGPo6sKVl54f/TVDObg1c=
+golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/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-20220425223048-2871e0cb64e4 h1:HVyaeDAYux4pnY+D/SiwmLOR36ewZ4iGQIIrtnuCjFA=
-golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
+golang.org/x/net v0.0.0-20220812174116-3211cb980234 h1:RDqmgfe7SvlMWoqC3xwQ2blLO3fcWcxMa3eBLRdRW7E=
+golang.org/x/net v0.0.0-20220812174116-3211cb980234/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f h1:Ax0t5p6N38Ga0dThY21weqDEyz2oklo4IvDkpigvkD8=
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190130150945-aca44879d564/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -266,15 +266,16 @@ golang.org/x/sys v0.0.0-20201207223542-d4d67f95c62d/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab h1:2QkjZIsXupsJbJIdSjjUOgWK3aEtzyuh2mPt3l/CkeU=
+golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/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.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
-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/time v0.0.0-20220722155302-e5dcc9cfc0b9 h1:ftMN5LMiBFjbzleLqtoBZk7KdJwhuybIU+FckUHgoyQ=
+golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9/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=
diff --git a/mvc/grpc.go b/mvc/grpc.go
index e3cbe86f..34f9c87b 100644
--- a/mvc/grpc.go
+++ b/mvc/grpc.go
@@ -47,7 +47,14 @@ func (g GRPC) Apply(c *ControllerActivator) {
return
}
- // If strict was false, allow common HTTP clients, consumes and produces JSON.
+ // If strict was true fires 404 on common HTTP clients.
+ if g.Strict {
+ ctx.NotFound()
+ ctx.StopExecution()
+ return
+ }
+
+ // Allow common HTTP clients, consumes and produces JSON.
ctx.Next()
}
diff --git a/sessions/sessiondb/redis/database.go b/sessions/sessiondb/redis/database.go
index 1773a208..3307b1af 100644
--- a/sessions/sessiondb/redis/database.go
+++ b/sessions/sessiondb/redis/database.go
@@ -30,6 +30,7 @@ type Config struct {
Addr string
// Clusters a list of network addresses for clusters.
// If not empty "Addr" is ignored and Redis clusters feature is used instead.
+ // Note that this field is ignored when setgging a custom `GoRedisClient`.
Clusters []string
// Use the specified Username to authenticate the current connection
// with one of the connections defined in the ACL list when connecting
@@ -267,7 +268,8 @@ func (db *Database) Delete(sid string, key string) (deleted bool) {
// Clear removes all session key values but it keeps the session entry.
func (db *Database) Clear(sid string) error {
- keys := db.keys(db.makeSID(sid))
+ sid = db.makeSID(sid)
+ keys := db.keys(sid)
for _, key := range keys {
if key == SessionIDKey {
continue
diff --git a/sessions/sessiondb/redis/driver_goredis.go b/sessions/sessiondb/redis/driver_goredis.go
index 6477cd75..50e994d0 100644
--- a/sessions/sessiondb/redis/driver_goredis.go
+++ b/sessions/sessiondb/redis/driver_goredis.go
@@ -27,7 +27,9 @@ type GoRedisClient interface {
// for the go-redis redis driver. See driver.go file.
type GoRedisDriver struct {
// Both Client and ClusterClient implements this interface.
- client GoRedisClient
+ // Custom one can be directly passed but if so, the
+ // Connect method does nothing (so all connection and client settings are ignored).
+ Client GoRedisClient
// Customize any go-redis fields manually
// before Connect.
ClientOptions Options
@@ -111,12 +113,24 @@ func (r *GoRedisDriver) mergeClusterOptions(c Config) *ClusterOptions {
return &opts
}
+// SetClient sets an existing go redis client to the sessions redis driver.
+//
+// Returns itself.
+func (r *GoRedisDriver) SetClient(goRedisClient GoRedisClient) *GoRedisDriver {
+ r.Client = goRedisClient
+ return r
+}
+
// Connect initializes the redis client.
func (r *GoRedisDriver) Connect(c Config) error {
+ if r.Client != nil { // if a custom one was given through SetClient.
+ return nil
+ }
+
if len(c.Clusters) > 0 {
- r.client = redis.NewClusterClient(r.mergeClusterOptions(c))
+ r.Client = redis.NewClusterClient(r.mergeClusterOptions(c))
} else {
- r.client = redis.NewClient(r.mergeClientOptions(c))
+ r.Client = redis.NewClient(r.mergeClientOptions(c))
}
return nil
@@ -125,29 +139,29 @@ func (r *GoRedisDriver) Connect(c Config) error {
// PingPong sends a ping message and reports whether
// the PONG message received successfully.
func (r *GoRedisDriver) PingPong() (bool, error) {
- pong, err := r.client.Ping(defaultContext).Result()
+ pong, err := r.Client.Ping(defaultContext).Result()
return pong == "PONG", err
}
// CloseConnection terminates the underline redis connection.
func (r *GoRedisDriver) CloseConnection() error {
- return r.client.Close()
+ return r.Client.Close()
}
// Set stores a "value" based on the session's "key".
// The value should be type of []byte, so unmarshal can happen.
func (r *GoRedisDriver) Set(sid, key string, value interface{}) error {
- return r.client.HSet(defaultContext, sid, key, value).Err()
+ return r.Client.HSet(defaultContext, sid, key, value).Err()
}
// Get returns the associated value of the session's given "key".
func (r *GoRedisDriver) Get(sid, key string) (interface{}, error) {
- return r.client.HGet(defaultContext, sid, key).Bytes()
+ return r.Client.HGet(defaultContext, sid, key).Bytes()
}
// Exists reports whether a session exists or not.
func (r *GoRedisDriver) Exists(sid string) bool {
- n, err := r.client.Exists(defaultContext, sid).Result()
+ n, err := r.Client.Exists(defaultContext, sid).Result()
if err != nil {
return false
}
@@ -157,7 +171,7 @@ func (r *GoRedisDriver) Exists(sid string) bool {
// TTL returns any TTL value of the session.
func (r *GoRedisDriver) TTL(sid string) time.Duration {
- dur, err := r.client.TTL(defaultContext, sid).Result()
+ dur, err := r.Client.TTL(defaultContext, sid).Result()
if err != nil {
return 0
}
@@ -167,29 +181,29 @@ func (r *GoRedisDriver) TTL(sid string) time.Duration {
// UpdateTTL sets expiration duration of the session.
func (r *GoRedisDriver) UpdateTTL(sid string, newLifetime time.Duration) error {
- _, err := r.client.Expire(defaultContext, sid, newLifetime).Result()
+ _, err := r.Client.Expire(defaultContext, sid, newLifetime).Result()
return err
}
// GetAll returns all the key values under the session.
func (r *GoRedisDriver) GetAll(sid string) (map[string]string, error) {
- return r.client.HGetAll(defaultContext, sid).Result()
+ return r.Client.HGetAll(defaultContext, sid).Result()
}
// GetKeys returns all keys under the session.
func (r *GoRedisDriver) GetKeys(sid string) ([]string, error) {
- return r.client.HKeys(defaultContext, sid).Result()
+ return r.Client.HKeys(defaultContext, sid).Result()
}
// Len returns the total length of key-values of the session.
func (r *GoRedisDriver) Len(sid string) int {
- return int(r.client.HLen(defaultContext, sid).Val())
+ return int(r.Client.HLen(defaultContext, sid).Val())
}
// Delete removes a value from the redis store.
func (r *GoRedisDriver) Delete(sid, key string) error {
if key == "" {
- return r.client.Del(defaultContext, sid).Err()
+ return r.Client.Del(defaultContext, sid).Err()
}
- return r.client.HDel(defaultContext, sid, key).Err()
+ return r.Client.HDel(defaultContext, sid, key).Err()
}
diff --git a/x/jsonx/iso8601.go b/x/jsonx/iso8601.go
index c5d22b8e..cae75ebe 100644
--- a/x/jsonx/iso8601.go
+++ b/x/jsonx/iso8601.go
@@ -1,18 +1,34 @@
package jsonx
import (
+ "database/sql/driver"
+ "errors"
"fmt"
"strconv"
"strings"
"time"
)
+var fixedEastUTCLocations = make(map[int]*time.Location)
+
+func registerFixedEastUTCLocation(name string, secondsFromUTC int) {
+ loc := time.FixedZone(name, secondsFromUTC)
+ fixedEastUTCLocations[secondsFromUTC] = loc
+}
+
+func init() {
+ registerFixedEastUTCLocation("EEST", 3*60*60) // + 3 hours.
+}
+
const (
// ISO8601Layout holds the time layout for the the javascript iso time.
// Read more at: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString.
ISO8601Layout = "2006-01-02T15:04:05"
// ISO8601ZLayout same as ISO8601Layout but with the timezone suffix.
ISO8601ZLayout = "2006-01-02T15:04:05Z"
+ // ISO8601ZUTCOffsetLayout ISO 8601 format, with full time and zone with UTC offset.
+ // Example: 2022-08-10T03:21:00.000000+03:00, 2022-08-09T00:00:00.000000.
+ ISO8601ZUTCOffsetLayout = "2006-01-02T15:04:05.999999Z07:00"
)
// ISO8601 describes a time compatible with javascript time format.
@@ -29,7 +45,28 @@ func ParseISO8601(s string) (ISO8601, error) {
err error
)
- if s[len(s)-1] == 'Z' {
+ if idx := strings.LastIndexFunc(s, startUTCOffsetIndexFunc); idx > 20 /* should have some distance, e.g. 26 */ {
+ length := parseSignedOffset(s[idx:])
+
+ if idx+1 > idx+length || len(s) <= idx+length+1 {
+ return ISO8601{}, fmt.Errorf("ISO8601: invalid timezone format: %s", s[idx:])
+ }
+
+ offsetText := s[idx+1 : idx+length]
+ offset, parseErr := strconv.Atoi(offsetText)
+ if parseErr != nil {
+ return ISO8601{}, err
+ }
+
+ // E.g. offset of +0300 is returned as 10800 which is - (3 * 60 * 60).
+ secondsEastUTC := offset * 60 * 60
+
+ if loc, ok := fixedEastUTCLocations[secondsEastUTC]; ok { // Specific (fixed) zone.
+ tt, err = time.ParseInLocation(ISO8601ZUTCOffsetLayout, s, loc)
+ } else { // Local or UTC.
+ tt, err = time.Parse(ISO8601ZUTCOffsetLayout, s)
+ }
+ } else if s[len(s)-1] == 'Z' {
tt, err = time.Parse(ISO8601ZLayout, s)
} else {
tt, err = time.Parse(ISO8601Layout, s)
@@ -39,7 +76,7 @@ func ParseISO8601(s string) (ISO8601, error) {
return ISO8601{}, err
}
- return ISO8601(tt.UTC()), nil
+ return ISO8601(tt), nil
}
// UnmarshalJSON parses the "b" into ISO8601 time.
@@ -90,6 +127,11 @@ func (t ISO8601) String() string {
return tt.Format(ISO8601Layout)
}
+// Value returns the database value of time.Time.
+func (t ISO8601) Value() (driver.Value, error) {
+ return time.Time(t), nil
+}
+
// Scan completes the sql driver.Scanner interface.
func (t *ISO8601) Scan(src interface{}) error {
switch v := src.(type) {
@@ -104,6 +146,8 @@ func (t *ISO8601) Scan(src interface{}) error {
return err
}
*t = tt
+ case []byte:
+ return t.Scan(string(v))
case nil:
*t = ISO8601(time.Time{})
default:
@@ -112,3 +156,54 @@ func (t *ISO8601) Scan(src interface{}) error {
return nil
}
+
+// parseSignedOffset parses a signed timezone offset (e.g. "+03" or "-04").
+// The function checks for a signed number in the range -23 through +23 excluding zero.
+// Returns length of the found offset string or 0 otherwise.
+//
+// Language internal function.
+func parseSignedOffset(value string) int {
+ sign := value[0]
+ if sign != '-' && sign != '+' {
+ return 0
+ }
+ x, rem, err := leadingInt(value[1:])
+
+ // fail if nothing consumed by leadingInt
+ if err != nil || value[1:] == rem {
+ return 0
+ }
+ if x > 23 {
+ return 0
+ }
+ return len(value) - len(rem)
+}
+
+var errLeadingInt = errors.New("ISO8601: time: bad [0-9]*") // never printed.
+
+// leadingInt consumes the leading [0-9]* from s.
+//
+// Language internal function.
+func leadingInt(s string) (x uint64, rem string, err error) {
+ i := 0
+ for ; i < len(s); i++ {
+ c := s[i]
+ if c < '0' || c > '9' {
+ break
+ }
+ if x > 1<<63/10 {
+ // overflow
+ return 0, "", errLeadingInt
+ }
+ x = x*10 + uint64(c) - '0'
+ if x > 1<<63 {
+ // overflow
+ return 0, "", errLeadingInt
+ }
+ }
+ return x, s[i:], nil
+}
+
+func startUTCOffsetIndexFunc(char rune) bool {
+ return char == '+' || char == '-'
+}
diff --git a/x/jsonx/iso8601_test.go b/x/jsonx/iso8601_test.go
new file mode 100644
index 00000000..cba8defd
--- /dev/null
+++ b/x/jsonx/iso8601_test.go
@@ -0,0 +1,81 @@
+package jsonx
+
+import (
+ "encoding/json"
+ "testing"
+ "time"
+)
+
+func TestISO8601(t *testing.T) {
+ data := `{"start": "2021-08-20T10:05:01", "end": "2021-12-01T17:05:06", "nothing": null, "empty": ""}`
+ v := struct {
+ Start ISO8601 `json:"start"`
+ End ISO8601 `json:"end"`
+ Nothing ISO8601 `json:"nothing"`
+ Empty ISO8601 `json:"empty"`
+ }{}
+ err := json.Unmarshal([]byte(data), &v)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if !v.Nothing.IsZero() {
+ t.Fatalf("expected 'nothing' to be zero but got: %v", v.Nothing)
+ }
+
+ if !v.Empty.IsZero() {
+ t.Fatalf("expected 'empty' to be zero but got: %v", v.Empty)
+ }
+
+ loc := time.UTC
+
+ if expected, got := time.Date(2021, time.August, 20, 10, 5, 1, 0, loc), v.Start.ToTime(); expected != got {
+ t.Fatalf("expected 'start' to be: %v but got: %v", expected, got)
+ }
+
+ if expected, got := time.Date(2021, time.December, 1, 17, 5, 6, 0, loc), v.End.ToTime(); expected != got {
+ t.Fatalf("expected 'start' to be: %v but got: %v", expected, got)
+ }
+}
+
+func TestISO8601WithZoneUTCOffset(t *testing.T) {
+ data := `{"start": "2022-08-10T03:21:00.000000+03:00", "end": "2022-08-10T09:49:00.000000+03:00", "nothing": null, "empty": ""}`
+ v := struct {
+ Start ISO8601 `json:"start"`
+ End ISO8601 `json:"end"`
+ Nothing ISO8601 `json:"nothing"`
+ Empty ISO8601 `json:"empty"`
+ }{}
+ err := json.Unmarshal([]byte(data), &v)
+ if err != nil {
+ t.Fatalf("unmarshal: %v", err)
+ }
+
+ // t.Logf("Start: %s, location: %s\n", v.Start.String(), v.Start.ToTime().Location().String())
+
+ if !v.Nothing.IsZero() {
+ t.Fatalf("expected 'nothing' to be zero but got: %v", v.Nothing)
+ }
+
+ if !v.Empty.IsZero() {
+ t.Fatalf("expected 'empty' to be zero but got: %v", v.Empty)
+ }
+
+ loc := time.FixedZone("EEST", 10800)
+
+ if expected, got := time.Date(2022, time.August, 10, 3, 21, 0, 0, loc).String(), v.Start.ToTime().String(); expected != got {
+ t.Fatalf("expected 'start' string to be: %v but got: %v", expected, got)
+ }
+
+ if expected, got := time.Date(2022, time.August, 10, 9, 49, 0, 0, loc).String(), v.End.ToTime().String(); expected != got {
+ t.Fatalf("expected 'start' string to be: %v but got: %v", expected, got)
+ }
+
+ if expected, got := time.Date(2022, time.August, 10, 3, 21, 0, 0, loc), v.Start.ToTime().In(loc); expected != got {
+ t.Fatalf("expected 'start' to be: %v but got: %v", expected, got)
+ }
+
+ if expected, got := time.Date(2022, time.August, 10, 9, 49, 0, 0, loc), v.End.ToTime().In(loc); expected != got {
+ t.Fatalf("expected 'start' to be: %v but got: %v", expected, got)
+ }
+}