diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 3c6f315..020dbc5 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -14,6 +14,17 @@ jobs: with: redis-version: 4 + - name: Start redis cluster + uses: vishnudxb/redis-cluster@1.0.9 + with: + master1-port: 5000 + master2-port: 5001 + master3-port: 5002 + slave1-port: 5003 + slave2-port: 5004 + slave3-port: 5005 + sleep-duration: 5 + - uses: actions/checkout@v3 - name: Set up Go @@ -28,11 +39,11 @@ jobs: restore-keys: | ${{ runner.os }}-go- + - name: Unit Test + run: go test -race -covermode=atomic -coverprofile=coverage.out ./... + - name: Lint run: | go vet -stdmethods=false $(go list ./...) go install mvdan.cc/gofumpt@v0.2.0 - test -z "$(gofumpt -l -extra .)" - - - name: Unit Test - run: go test -race -covermode=atomic -coverprofile=coverage.out ./... + test -z "$(gofumpt -l -extra .)" \ No newline at end of file diff --git a/README.md b/README.md index 48e5947..db86f81 100644 --- a/README.md +++ b/README.md @@ -178,6 +178,44 @@ func main() { } ``` +## Redis cluster + +```go +package main + +import ( + "context" + + "github.com/cloudwego/hertz/pkg/app" + "github.com/cloudwego/hertz/pkg/app/server" + "github.com/cloudwego/hertz/pkg/common/utils" + "github.com/hertz-contrib/sessions" + "github.com/hertz-contrib/sessions/rediscluster" +) + +func main() { + h := server.Default(server.WithHostPorts(":8000")) + store, _ := rediscluster.NewStore(10, []string{"localhost:5001", "localhost:5002"}, "", nil, []byte("secret")) + h.Use(sessions.New("mysession", store)) + + h.GET("/incr", func(ctx context.Context, c *app.RequestContext) { + session := sessions.Default(c) + var count int + v := session.Get("count") + if v == nil { + count = 0 + } else { + count = v.(int) + count++ + } + session.Set("count", count) + session.Save() + c.JSON(200, utils.H{"count": count}) + }) + h.Spin() +} +``` + ## License This project is under Apache License. See the [LICENSE](LICENSE) file for the full license text. diff --git a/README_CN.md b/README_CN.md index b8ff124..e905572 100644 --- a/README_CN.md +++ b/README_CN.md @@ -178,6 +178,44 @@ func main() { } ``` +## Redis 集群 + +```go +package main + +import ( + "context" + + "github.com/cloudwego/hertz/pkg/app" + "github.com/cloudwego/hertz/pkg/app/server" + "github.com/cloudwego/hertz/pkg/common/utils" + "github.com/hertz-contrib/sessions" + "github.com/hertz-contrib/sessions/rediscluster" +) + +func main() { + h := server.Default(server.WithHostPorts(":8000")) + store, _ := rediscluster.NewStore(10, []string{"localhost:5001", "localhost:5002"}, "", nil, []byte("secret")) + h.Use(sessions.New("mysession", store)) + + h.GET("/incr", func(ctx context.Context, c *app.RequestContext) { + session := sessions.Default(c) + var count int + v := session.Get("count") + if v == nil { + count = 0 + } else { + count = v.(int) + count++ + } + session.Set("count", count) + session.Save() + c.JSON(200, utils.H{"count": count}) + }) + h.Spin() +} +``` + ## 许可证 本项目采用Apache许可证。参见 [LICENSE](LICENSE) 文件中的完整许可证文本。 diff --git a/_example/redis_cluster/main.go b/_example/redis_cluster/main.go new file mode 100644 index 0000000..f93e48e --- /dev/null +++ b/_example/redis_cluster/main.go @@ -0,0 +1,73 @@ +/* + * Copyright 2023 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * The MIT License (MIT) + * + * Copyright (c) 2016 Bo-Yi Wu + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * +* This file may have been modified by CloudWeGo authors. All CloudWeGo +* Modifications are Copyright 2022 CloudWeGo Authors. +*/ + +package main + +import ( + "context" + + "github.com/cloudwego/hertz/pkg/app" + "github.com/cloudwego/hertz/pkg/app/server" + "github.com/cloudwego/hertz/pkg/common/utils" + "github.com/hertz-contrib/sessions" + "github.com/hertz-contrib/sessions/rediscluster" +) + +func main() { + h := server.Default(server.WithHostPorts(":8000")) + store, _ := rediscluster.NewStore(10, []string{"localhost:5001", "localhost:5002"}, "", nil, []byte("secret")) + h.Use(sessions.New("mysession", store)) + + h.GET("/incr", func(ctx context.Context, c *app.RequestContext) { + session := sessions.Default(c) + var count int + v := session.Get("count") + if v == nil { + count = 0 + } else { + count = v.(int) + count++ + } + session.Set("count", count) + session.Save() + c.JSON(200, utils.H{"count": count}) + }) + h.Spin() +} diff --git a/go.mod b/go.mod index be56070..153a81d 100644 --- a/go.mod +++ b/go.mod @@ -3,9 +3,10 @@ module github.com/hertz-contrib/sessions go 1.16 require ( - github.com/cloudwego/hertz v0.6.2 - github.com/gomodule/redigo v2.0.0+incompatible - github.com/gorilla/context v1.1.1 - github.com/gorilla/securecookie v1.1.1 - github.com/gorilla/sessions v1.2.1 + github.com/cloudwego/hertz v0.7.2 + github.com/gomodule/redigo v1.8.9 + github.com/gorilla/context v1.1.2 + github.com/gorilla/securecookie v1.1.2 + github.com/gorilla/sessions v1.2.2 + github.com/redis/go-redis/v9 v9.3.0 ) diff --git a/go.sum b/go.sum index c7fbae4..0a39a0b 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,7 @@ +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= github.com/bytedance/go-tagexpr/v2 v2.9.2 h1:QySJaAIQgOEDQBLS3x9BxOWrnhqu5sQ+f6HaZIxD39I= github.com/bytedance/go-tagexpr/v2 v2.9.2/go.mod h1:5qsx05dYOiUXOUgnQ7w3Oz8BYs2qtM/bJokdLb79wRM= github.com/bytedance/gopkg v0.0.0-20220413063733-65bf48ffb3a7 h1:PtwsQyQJGxf8iaPptPNaduEIu9BnrNms+pcRdHAxZaM= @@ -7,33 +11,39 @@ github.com/bytedance/mockey v1.2.1/go.mod h1:+Jm/fzWZAuhEDrPXVjDf/jLM2BlLXJkwk94 github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= github.com/bytedance/sonic v1.8.1 h1:NqAHCaGaTzro0xMmnTCLUyRlbEP6r8MCA1cJUrH3Pu4= github.com/bytedance/sonic v1.8.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= -github.com/cloudwego/hertz v0.6.2 h1:8NM0yHbyv8B4dNYgICirk733S7monTNB+uR9as1It1Y= -github.com/cloudwego/hertz v0.6.2/go.mod h1:2em2hGREvCBawsTQcQxyWBGVlCeo+N1pp2q0HkkbwR0= -github.com/cloudwego/netpoll v0.3.1 h1:xByoORmCLIyKZ8gS+da06WDo3j+jvmhaqS2KeKejtBk= -github.com/cloudwego/netpoll v0.3.1/go.mod h1:1T2WVuQ+MQw6h6DpE45MohSvDTKdy2DlzCx2KsnPI4E= +github.com/cloudwego/hertz v0.7.2 h1:3Wrm6AWK4EBaXXqvyG8RahafHgcxZ21WFsosBBoobQ0= +github.com/cloudwego/hertz v0.7.2/go.mod h1:WliNtVbwihWHHgAaIQEbVXl0O3aWj0ks1eoPrcEAnjs= +github.com/cloudwego/netpoll v0.5.0 h1:oRrOp58cPCvK2QbMozZNDESvrxQaEHW2dCimmwH1lcU= +github.com/cloudwego/netpoll v0.5.0/go.mod h1:xVefXptcyheopwNDZjDPcfU6kIjZXZ4nY550k1yH9eQ= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0= -github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= +github.com/gomodule/redigo v1.8.9 h1:Sl3u+2BI/kk+VEatbj0scLdrFhjPmbxOc1myhDP41ws= +github.com/gomodule/redigo v1.8.9/go.mod h1:7ArFNvsTjH8GMMzB4uy1snslv2BwmginuMs06a1uzZE= github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8= -github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= -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/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI= -github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= +github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o= +github.com/gorilla/context v1.1.2/go.mod h1:KDPwT9i/MeWHiLl90fuTgrt4/wPcv75vFAZLaOOcbxM= +github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= +github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= +github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTjY= +github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ= github.com/henrylee2cn/ameda v1.4.8/go.mod h1:liZulR8DgHxdK+MEwvZIylGnmcjzQ6N6f2PlWe7nEO4= github.com/henrylee2cn/ameda v1.4.10 h1:JdvI2Ekq7tapdPsuhrc4CaFiqw6QXFvZIULWJgQyCAk= github.com/henrylee2cn/ameda v1.4.10/go.mod h1:liZulR8DgHxdK+MEwvZIylGnmcjzQ6N6f2PlWe7nEO4= @@ -47,6 +57,8 @@ github.com/nyaruka/phonenumbers v1.0.55 h1:bj0nTO88Y68KeUQ/n3Lo2KgK7lM1hF7L9NFuw github.com/nyaruka/phonenumbers v1.0.55/go.mod h1:sDaTZ/KPX5f8qyV9qN+hIm+4ZBARJrupC6LuhshJq1U= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/redis/go-redis/v9 v9.3.0 h1:RiVDjmig62jIWp7Kk4XVLs0hzV6pI3PyTnnL0cnn0u0= +github.com/redis/go-redis/v9 v9.3.0/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= @@ -62,8 +74,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/tidwall/gjson v1.9.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= -github.com/tidwall/gjson v1.13.0 h1:3TFY9yxOQShrvmjdM76K+jc66zJeT6D3/VFFYCGQf7M= -github.com/tidwall/gjson v1.13.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM= +github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= diff --git a/licenses/LICENCE-go-redis b/licenses/LICENCE-go-redis new file mode 100644 index 0000000..d46a573 --- /dev/null +++ b/licenses/LICENCE-go-redis @@ -0,0 +1,25 @@ +Copyright (c) 2013 The github.com/redis/go-redis Authors. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/redis/redistore.go b/redis/redistore.go index 9fdfdd2..e8ab131 100644 --- a/redis/redistore.go +++ b/redis/redistore.go @@ -47,12 +47,8 @@ package redis import ( - "bytes" "encoding/base32" - "encoding/gob" - "encoding/json" "errors" - "fmt" "net/http" "strings" "time" @@ -62,69 +58,12 @@ import ( "github.com/gomodule/redigo/redis" "github.com/gorilla/securecookie" "github.com/gorilla/sessions" + hs "github.com/hertz-contrib/sessions" ) // Amount of time for cookies/redis keys to expire. var sessionExpire = 86400 * 30 -// SessionSerializer provides an interface hook for alternative serializers -type SessionSerializer interface { - Deserialize(d []byte, ss *sessions.Session) error - Serialize(ss *sessions.Session) ([]byte, error) -} - -// JSONSerializer encode the session map to JSON. -type JSONSerializer struct{} - -// Serialize to JSON. Will err if there are unmarshalable key values -func (s JSONSerializer) Serialize(ss *sessions.Session) ([]byte, error) { - m := make(map[string]interface{}, len(ss.Values)) - for k, v := range ss.Values { - ks, ok := k.(string) - if !ok { - err := fmt.Errorf("non-string key value, cannot serialize session to JSON: %v", k) - hlog.Errorf("redistore.JSONSerializer.serialize() Error: %v", err) - return nil, err - } - m[ks] = v - } - return json.Marshal(m) -} - -// Deserialize back to map[string]interface{} -func (s JSONSerializer) Deserialize(d []byte, ss *sessions.Session) error { - m := make(map[string]interface{}) - err := json.Unmarshal(d, &m) - if err != nil { - hlog.Errorf("redistore.JSONSerializer.deserialize() Error: %v", err) - return err - } - for k, v := range m { - ss.Values[k] = v - } - return nil -} - -// GobSerializer uses gob package to encode the session map -type GobSerializer struct{} - -// Serialize using gob -func (s GobSerializer) Serialize(ss *sessions.Session) ([]byte, error) { - buf := new(bytes.Buffer) - enc := gob.NewEncoder(buf) - err := enc.Encode(ss.Values) - if err == nil { - return buf.Bytes(), nil - } - return nil, err -} - -// Deserialize back to map[interface{}]interface{} -func (s GobSerializer) Deserialize(d []byte, ss *sessions.Session) error { - dec := gob.NewDecoder(bytes.NewBuffer(d)) - return dec.Decode(&ss.Values) -} - // RediStore stores sessions in a redis backend. type RediStore struct { Pool *redis.Pool @@ -133,7 +72,7 @@ type RediStore struct { DefaultMaxAge int // default Redis TTL for a MaxAge == 0 session maxLength int keyPrefix string - serializer SessionSerializer + serializer hs.Serializer } // SetMaxLength sets RediStore.maxLength if the `l` argument is greater or equal 0 @@ -154,7 +93,7 @@ func (s *RediStore) SetKeyPrefix(p string) { } // SetSerializer sets the serializer -func (s *RediStore) SetSerializer(ss SessionSerializer) { +func (s *RediStore) SetSerializer(ss hs.Serializer) { s.serializer = ss } @@ -253,7 +192,7 @@ func NewRediStoreWithPool(pool *redis.Pool, keyPairs ...[]byte) (*RediStore, err DefaultMaxAge: 60 * 20, // 20 minutes seems like a reasonable default maxLength: 4096, keyPrefix: "session_", - serializer: GobSerializer{}, + serializer: hs.GobSerializer{}, } _, err := rs.ping() return rs, err diff --git a/redis/redistore_test.go b/redis/redistore_test.go index fb6133e..69e5547 100644 --- a/redis/redistore_test.go +++ b/redis/redistore_test.go @@ -54,6 +54,8 @@ import ( "net/http/httptest" "testing" + hs "github.com/hertz-contrib/sessions" + "github.com/gorilla/sessions" ) @@ -415,7 +417,7 @@ func TestRediStore(t *testing.T) { { addr := setup() store, err := NewRediStore(10, "tcp", addr, "", []byte("secret-key")) - store.SetSerializer(JSONSerializer{}) + store.SetSerializer(hs.JSONSerializer{}) if err != nil { t.Fatal(err.Error()) } diff --git a/rediscluster/redisc_store.go b/rediscluster/redisc_store.go new file mode 100644 index 0000000..a46f0ec --- /dev/null +++ b/rediscluster/redisc_store.go @@ -0,0 +1,252 @@ +/* + * Copyright 2023 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * Copyright (c) 2012 Rodrigo Moraes. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * +* This file may have been modified by CloudWeGo authors. All CloudWeGo +* Modifications are Copyright 2022 CloudWeGo Authors. +*/ + +package rediscluster + +import ( + "context" + "encoding/base32" + "errors" + "net/http" + "strings" + "time" + + "github.com/gorilla/securecookie" + "github.com/gorilla/sessions" + hs "github.com/hertz-contrib/sessions" + "github.com/redis/go-redis/v9" +) + +var sessionExpire = 86400 * 30 + +type Store struct { + Rdb *redis.ClusterClient + Codecs []securecookie.Codec + Opts *sessions.Options // default configuration + DefaultMaxAge int // default Redis TTL for a MaxAge == 0 session + maxLength int + keyPrefix string + serializer hs.Serializer +} + +func (s *Store) Options(options hs.Options) { + s.Opts = options.ToGorillaOptions() +} + +// NewStoreWithOption returns a new rediscluster.Store by setting *redis.ClusterOptions +func NewStoreWithOption(opt *redis.ClusterOptions, kvs ...[]byte) (*Store, error) { + rs := &Store{ + Rdb: redis.NewClusterClient(opt), + Codecs: securecookie.CodecsFromPairs(kvs...), + Opts: &sessions.Options{ + Path: "/", + MaxAge: sessionExpire, + }, + DefaultMaxAge: 60 * 20, // 20 minutes seems like a reasonable default + maxLength: 4096, + keyPrefix: "session_", + serializer: hs.GobSerializer{}, + } + err := rs.Rdb.ForEachShard(context.Background(), func(ctx context.Context, shard *redis.Client) error { + return shard.Ping(ctx).Err() + }) + return rs, err +} + +// NewStore returns a new rediscluster.Store +func NewStore(maxIdle int, addrs []string, password string, newClient func(opt *redis.Options) *redis.Client, kvs ...[]byte) (*Store, error) { + return NewStoreWithOption(newOption(addrs, password, maxIdle, newClient), kvs...) +} + +func (s *Store) Get(r *http.Request, name string) (*sessions.Session, error) { + return sessions.GetRegistry(r).Get(s, name) +} + +func (s *Store) New(r *http.Request, name string) (*sessions.Session, error) { + var ( + err error + ok bool + ) + session := sessions.NewSession(s, name) + // make a copy + options := *s.Opts + session.Options = &options + session.IsNew = true + if c, errCookie := r.Cookie(name); errCookie == nil { + err = securecookie.DecodeMulti(name, c.Value, &session.ID, s.Codecs...) + if err == nil { + ok, err = s.load(session) + session.IsNew = !(err == nil && ok) // not new if no error and data available + } + } + return session, err +} + +func (s *Store) Save(r *http.Request, w http.ResponseWriter, session *sessions.Session) error { + // Marked for deletion. + if session.Options.MaxAge <= 0 { + if err := s.delete(session); err != nil { + return err + } + http.SetCookie(w, sessions.NewCookie(session.Name(), "", session.Options)) + } else { + // Build an alphanumeric key for the redis store. + if session.ID == "" { + session.ID = strings.TrimRight(base32.StdEncoding.EncodeToString(securecookie.GenerateRandomKey(32)), "=") + } + if err := s.save(session); err != nil { + return err + } + encoded, err := securecookie.EncodeMulti(session.Name(), session.ID, s.Codecs...) + if err != nil { + return err + } + http.SetCookie(w, sessions.NewCookie(session.Name(), encoded, session.Options)) + } + return nil +} + +func (s *Store) Close() error { + return s.Rdb.Close() +} + +// SetMaxLength sets RedisClusterStore.maxLength if the `l` argument is greater or equal 0 +// maxLength restricts the maximum length of new sessions to l. +// If l is 0 there is no limit to the size of a session, use with caution. +// The default for a new RedisClusterStore is 4096. Redis allows for max. +// value sizes of up to 512MB (http://redis.io/topics/data-types) +// Default: 4096, +func (s *Store) SetMaxLength(l int) { + if l >= 0 { + s.maxLength = l + } +} + +// SetKeyPrefix set the prefix +func (s *Store) SetKeyPrefix(p string) { + s.keyPrefix = p +} + +// SetSerializer sets the serializer +func (s *Store) SetSerializer(ss hs.Serializer) { + s.serializer = ss +} + +func (s *Store) load(session *sessions.Session) (bool, error) { + res := s.Rdb.Get(context.Background(), s.keyPrefix+session.ID) + if res == nil { + return false, nil + } + b, err := res.Bytes() + if err != nil { + return false, err + } + return true, s.serializer.Deserialize(b, session) +} + +// save stores the session in redis. +func (s *Store) save(session *sessions.Session) error { + b, err := s.serializer.Serialize(session) + if err != nil { + return err + } + if s.maxLength != 0 && len(b) > s.maxLength { + return errors.New("SessionStore: the value to store is too big") + } + age := session.Options.MaxAge + if age == 0 { + age = s.DefaultMaxAge + } + err = s.Rdb.SetEx(context.Background(), s.keyPrefix+session.ID, b, time.Duration(age)*time.Second).Err() + return err +} + +func (s *Store) ping() (bool, error) { + res := s.Rdb.Ping(context.Background()) + if result, err := res.Result(); result != "PONG" || err != nil { + return false, err + } + return true, nil +} + +func (s *Store) delete(session *sessions.Session) error { + del := s.Rdb.Del(context.Background(), s.keyPrefix+session.ID) + return del.Err() +} + +// LoadSessionBySessionId Get session using session_id even without a context +func LoadSessionBySessionId(s *Store, sessionId string) (*sessions.Session, error) { + var session sessions.Session + session.ID = sessionId + exist, err := s.load(&session) + if err != nil { + return nil, err + } + if !exist { + return nil, nil + } + return &session, nil +} + +// SaveSessionWithoutContext Save session even without a context +func SaveSessionWithoutContext(s *Store, sessionId string, session *sessions.Session) error { + session.ID = sessionId + return s.save(session) +} + +func newOption( + addrs []string, + password string, + maxIdleConns int, + newClient func(opt *redis.Options) *redis.Client, +) *redis.ClusterOptions { + return &redis.ClusterOptions{ + Addrs: addrs, + NewClient: newClient, + Password: password, + MaxIdleConns: maxIdleConns, + } +} diff --git a/rediscluster/redisc_test.go b/rediscluster/redisc_test.go new file mode 100644 index 0000000..4156d8b --- /dev/null +++ b/rediscluster/redisc_test.go @@ -0,0 +1,375 @@ +/* + * Copyright 2023 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * Copyright (c) 2012 Rodrigo Moraes. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * +* This file may have been modified by CloudWeGo authors. All CloudWeGo +* Modifications are Copyright 2022 CloudWeGo Authors. +*/ + +package rediscluster + +import ( + "bytes" + "encoding/base64" + "encoding/gob" + "net/http" + "net/http/httptest" + "testing" + + "github.com/cloudwego/hertz/pkg/common/test/assert" + "github.com/gorilla/sessions" +) + +func init() { + gob.Register(FlashMessage{}) +} + +// ---------------------------------------------------------------------------- +// ResponseRecorder +// ---------------------------------------------------------------------------- +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// ResponseRecorder is an implementation of http.ResponseWriter that +// records its mutations for later inspection in tests. +type ResponseRecorder struct { + Code int // the HTTP response code from WriteHeader + HeaderMap http.Header // the HTTP response headers + Body *bytes.Buffer // if non-nil, the bytes.Buffer to append written data to + Flushed bool +} + +// NewRecorder returns an initialized ResponseRecorder. +func NewRecorder() *ResponseRecorder { + return &ResponseRecorder{ + HeaderMap: make(http.Header), + Body: new(bytes.Buffer), + } +} + +// Header returns the response headers. +func (rw *ResponseRecorder) Header() http.Header { + return rw.HeaderMap +} + +// Write always succeeds and writes to rw.Body, if not nil. +func (rw *ResponseRecorder) Write(buf []byte) (int, error) { + if rw.Body != nil { + rw.Body.Write(buf) + } + if rw.Code == 0 { + rw.Code = http.StatusOK + } + return len(buf), nil +} + +// WriteHeader sets rw.Code. +func (rw *ResponseRecorder) WriteHeader(code int) { + rw.Code = code +} + +// Flush sets rw.Flushed to true. +func (rw *ResponseRecorder) Flush() { + rw.Flushed = true +} + +// ---------------------------------------------------------------------------- + +type FlashMessage struct { + Type int + Message string +} + +func TestNewRedisCluster(t *testing.T) { + var ( + req *http.Request + rsp *ResponseRecorder + hdr http.Header + ok bool + cookies []string + session *sessions.Session + flashes []interface{} + ) + + { + store, err := NewStore(10, []string{"localhost:5000", "localhost:5001"}, "", nil, []byte("secret-key")) + + assert.Nil(t, err) + defer store.Close() + + req, _ = http.NewRequest("GET", "http://localhost:8080/", nil) + rsp = NewRecorder() + // Get a session. + if session, err = store.Get(req, "session-key"); err != nil { + t.Fatalf("Error getting session: %v", err) + } + // Get a flash. + flashes = session.Flashes() + if len(flashes) != 0 { + t.Errorf("Expected empty flashes; Got %v", flashes) + } + // Add some flashes. + session.AddFlash("foo") + session.AddFlash("bar") + // Custom key. + session.AddFlash("baz", "custom_key") + // Save. + if err = sessions.Save(req, rsp); err != nil { + t.Fatalf("Error saving session: %v", err) + } + hdr = rsp.Header() + cookies, ok = hdr["Set-Cookie"] + if !ok || len(cookies) != 1 { + t.Fatalf("No cookies. Header: %s", hdr) + } + } + + { + store, err := NewStore(10, []string{"localhost:5000", "localhost:5001"}, "", nil, []byte("secret-key")) + if err != nil { + t.Fatal(err.Error()) + } + defer store.Close() + + req, _ := http.NewRequest("GET", "http://localhost:8080/", nil) + req.Header.Add("Cookie", cookies[0]) + rsp = NewRecorder() + // Get a session. + if session, err = store.Get(req, "session-key"); err != nil { + t.Fatalf("Error getting session: %v", err) + } + // Check all saved values. + flashes = session.Flashes() + if len(flashes) != 2 { + t.Fatalf("Expected flashes; Got %v", flashes) + } + if flashes[0] != "foo" || flashes[1] != "bar" { + t.Errorf("Expected foo,bar; Got %v", flashes) + } + flashes = session.Flashes() + if len(flashes) != 0 { + t.Errorf("Expected dumped flashes; Got %v", flashes) + } + // Custom key. + flashes = session.Flashes("custom_key") + if len(flashes) != 1 { + t.Errorf("Expected flashes; Got %v", flashes) + } else if flashes[0] != "baz" { + t.Errorf("Expected baz; Got %v", flashes) + } + flashes = session.Flashes("custom_key") + if len(flashes) != 0 { + t.Errorf("Expected dumped flashes; Got %v", flashes) + } + + // RediStore specific + // Set MaxAge to -1 to mark for deletion. + session.Options.MaxAge = -1 + // Save. + if err = sessions.Save(req, rsp); err != nil { + t.Fatalf("Error saving session: %v", err) + } + } + + { + store, err := NewStore(10, []string{"localhost:5000", "localhost:5001"}, "", nil, []byte("secret-key")) + if err != nil { + t.Fatal(err.Error()) + } + defer store.Close() + req, _ = http.NewRequest("GET", "http://localhost:8080/", nil) + rsp = NewRecorder() + // Get a session. + if session, err = store.Get(req, "session-key"); err != nil { + t.Fatalf("Error getting session: %v", err) + } + // Get a flash. + flashes = session.Flashes() + if len(flashes) != 0 { + t.Errorf("Expected empty flashes; Got %v", flashes) + } + // Add some flashes. + session.AddFlash(&FlashMessage{42, "foo"}) + // Save. + if err = sessions.Save(req, rsp); err != nil { + t.Fatalf("Error saving session: %v", err) + } + hdr = rsp.Header() + cookies, ok = hdr["Set-Cookie"] + if !ok || len(cookies) != 1 { + t.Fatalf("No cookies. Header: %s", hdr) + } + + sID := session.ID + s, err := LoadSessionBySessionId(store, sID) + if err != nil { + t.Fatalf("LoadSessionBySessionId Error: %v", err) + } + + if len(s.Values) == 0 { + t.Fatalf("No session value: %s", hdr) + } + } + + { + store, err := NewStore(10, []string{"localhost:5000", "localhost:5001"}, "", nil, []byte("secret-key")) + if err != nil { + t.Fatal(err.Error()) + } + defer store.Close() + + req, _ := http.NewRequest("GET", "http://localhost:8080/", nil) + req.Header.Add("Cookie", cookies[0]) + rsp = NewRecorder() + // Get a session. + if session, err = store.Get(req, "session-key"); err != nil { + t.Fatalf("Error getting session: %v", err) + } + // Check all saved values. + flashes = session.Flashes() + if len(flashes) != 1 { + t.Fatalf("Expected flashes; Got %v", flashes) + } + custom := flashes[0].(FlashMessage) + if custom.Type != 42 || custom.Message != "foo" { + t.Errorf("Expected %#v, got %#v", FlashMessage{42, "foo"}, custom) + } + + // RediStore specific + // Set MaxAge to -1 to mark for deletion. + session.Options.MaxAge = -1 + // Save. + if err = sessions.Save(req, rsp); err != nil { + t.Fatalf("Error saving session: %v", err) + } + } + + { + store, err := NewStore(10, []string{"localhost:5000", "localhost:5001"}, "", nil, []byte("secret-key")) + if err != nil { + t.Fatal(err.Error()) + } + defer store.Close() + + req, err = http.NewRequest("GET", "http://www.example.com", nil) + if err != nil { + t.Fatal("failed to create request", err) + } + w := httptest.NewRecorder() + + session, err = store.New(req, "my session") + if err != nil { + t.Fatal("failed to New store", err) + } + session.Values["big"] = make([]byte, base64.StdEncoding.DecodedLen(4096*2)) + err = session.Save(req, w) + if err == nil { + t.Fatal("expected an error, got nil") + } + + store.SetMaxLength(4096 * 3) // A bit more than the value size to account for encoding overhead. + err = session.Save(req, w) + if err != nil { + t.Fatal("failed to Save:", err) + } + } + + { + store, err := NewStore(10, []string{"localhost:5000", "localhost:5001"}, "", nil, []byte("secret-key")) + if err != nil { + t.Fatal(err.Error()) + } + defer store.Close() + + req, _ = http.NewRequest("GET", "http://localhost:8080/", nil) + rsp = NewRecorder() + // Get a session. + if session, err = store.Get(req, "session-key"); err != nil { + t.Fatalf("Error getting session: %v", err) + } + // Get a flash. + flashes = session.Flashes() + if len(flashes) != 0 { + t.Errorf("Expected empty flashes; Got %v", flashes) + } + // Add some flashes. + session.AddFlash("foo") + // Save. + if err = sessions.Save(req, rsp); err != nil { + t.Fatalf("Error saving session: %v", err) + } + hdr = rsp.Header() + cookies, ok = hdr["Set-Cookie"] + if !ok || len(cookies) != 1 { + t.Fatalf("No cookies. Header: %s", hdr) + } + + // Get a session. + req.Header.Add("Cookie", cookies[0]) + if session, err = store.Get(req, "session-key"); err != nil { + t.Fatalf("Error getting session: %v", err) + } + // Check all saved values. + flashes = session.Flashes() + if len(flashes) != 1 { + t.Fatalf("Expected flashes; Got %v", flashes) + } + if flashes[0] != "foo" { + t.Errorf("Expected foo,bar; Got %v", flashes) + } + } +} + +func TestPingGoodPort(t *testing.T) { + store, err := NewStore(10, []string{"localhost:5000", "localhost:5001"}, "", nil, []byte("secret-key")) + if err != nil { + t.Error(err.Error()) + } + defer store.Close() + ok, err := store.ping() + if err != nil { + t.Error(err.Error()) + } + if !ok { + t.Error("Expected server to PONG") + } +} diff --git a/serializer.go b/serializer.go new file mode 100644 index 0000000..17e4126 --- /dev/null +++ b/serializer.go @@ -0,0 +1,115 @@ +/* + * Copyright 2023 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * Copyright (c) 2012 Rodrigo Moraes. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * +* This file may have been modified by CloudWeGo authors. All CloudWeGo +* Modifications are Copyright 2022 CloudWeGo Authors. +*/ + +package sessions + +import ( + "bytes" + "encoding/gob" + "encoding/json" + "fmt" + + "github.com/cloudwego/hertz/pkg/common/hlog" + "github.com/gorilla/sessions" +) + +// Serializer provides an interface hook for alternative serializers +type Serializer interface { + Deserialize(d []byte, ss *sessions.Session) error + Serialize(ss *sessions.Session) ([]byte, error) +} + +// JSONSerializer encode the session map to JSON. +type JSONSerializer struct{} + +// Serialize to JSON. Will err if there are unmarshalable key values +func (s JSONSerializer) Serialize(ss *sessions.Session) ([]byte, error) { + m := make(map[string]interface{}, len(ss.Values)) + for k, v := range ss.Values { + ks, ok := k.(string) + if !ok { + err := fmt.Errorf("non-string key value, cannot serialize session to JSON: %v", k) + hlog.Errorf("redistore.JSONSerializer.serialize() Error: %v", err) + return nil, err + } + m[ks] = v + } + return json.Marshal(m) +} + +// Deserialize back to map[string]interface{} +func (s JSONSerializer) Deserialize(d []byte, ss *sessions.Session) error { + m := make(map[string]interface{}) + err := json.Unmarshal(d, &m) + if err != nil { + hlog.Errorf("redistore.JSONSerializer.deserialize() Error: %v", err) + return err + } + for k, v := range m { + ss.Values[k] = v + } + return nil +} + +// GobSerializer uses gob package to encode the session map +type GobSerializer struct{} + +// Serialize using gob +func (s GobSerializer) Serialize(ss *sessions.Session) ([]byte, error) { + buf := new(bytes.Buffer) + enc := gob.NewEncoder(buf) + err := enc.Encode(ss.Values) + if err == nil { + return buf.Bytes(), nil + } + return nil, err +} + +// Deserialize back to map[interface{}]interface{} +func (s GobSerializer) Deserialize(d []byte, ss *sessions.Session) error { + dec := gob.NewDecoder(bytes.NewBuffer(d)) + return dec.Decode(&ss.Values) +}