root: replace boj/redistore with vendored version of rbcervilla/redisstore (#6988)
* root: replace boj/redistore with vendored version of rbcervilla/redisstore Signed-off-by: Jens Langhammer <jens@goauthentik.io> * setup env for go tests Signed-off-by: Jens Langhammer <jens@goauthentik.io> --------- Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
parent
90aa5409cd
commit
c93c6ee6f9
|
@ -39,6 +39,8 @@ jobs:
|
|||
- uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
- name: Setup authentik env
|
||||
uses: ./.github/actions/setup
|
||||
- name: Generate API
|
||||
run: make gen-client-go
|
||||
- name: Go unittests
|
||||
|
|
4
go.mod
4
go.mod
|
@ -6,7 +6,6 @@ require (
|
|||
beryju.io/ldap v0.1.0
|
||||
github.com/Netflix/go-env v0.0.0-20210215222557-e437a7e7f9fb
|
||||
github.com/coreos/go-oidc v2.2.1+incompatible
|
||||
github.com/garyburd/redigo v1.6.4
|
||||
github.com/getsentry/sentry-go v0.24.1
|
||||
github.com/go-http-utils/etag v0.0.0-20161124023236-513ea8f21eb1
|
||||
github.com/go-ldap/ldap/v3 v3.4.6
|
||||
|
@ -23,6 +22,7 @@ require (
|
|||
github.com/nmcclain/asn1-ber v0.0.0-20170104154839-2661553a0484
|
||||
github.com/pires/go-proxyproto v0.7.0
|
||||
github.com/prometheus/client_golang v1.16.0
|
||||
github.com/redis/go-redis/v9 v9.2.0
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/spf13/cobra v1.7.0
|
||||
github.com/stretchr/testify v1.8.4
|
||||
|
@ -30,7 +30,6 @@ require (
|
|||
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
|
||||
golang.org/x/oauth2 v0.12.0
|
||||
golang.org/x/sync v0.3.0
|
||||
gopkg.in/boj/redistore.v1 v1.0.0-20160128113310-fc113767cd6b
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
layeh.com/radius v0.0.0-20210819152912-ad72663a72ab
|
||||
)
|
||||
|
@ -41,6 +40,7 @@ require (
|
|||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/felixge/httpsnoop v1.0.1 // indirect
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.5 // indirect
|
||||
github.com/go-http-utils/fresh v0.0.0-20161124030543-7231e26a4b27 // indirect
|
||||
|
|
10
go.sum
10
go.sum
|
@ -48,6 +48,8 @@ github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3d
|
|||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
|
@ -63,14 +65,14 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
|
|||
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/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ=
|
||||
github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/garyburd/redigo v1.6.4 h1:LFu2R3+ZOPgSMWMOL+saa/zXRjw0ID2G8FepO53BGlg=
|
||||
github.com/garyburd/redigo v1.6.4/go.mod h1:rTb6epsqigu3kYKBnaF028A7Tf/Aw5s0cqA47doKKqw=
|
||||
github.com/getsentry/sentry-go v0.24.1 h1:W6/0GyTy8J6ge6lVCc94WB6Gx2ZuLrgopnn9w8Hiwuk=
|
||||
github.com/getsentry/sentry-go v0.24.1/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.5 h1:MNHlNMBDgEKD4TcKr36vQN68BA00aDfjIt3/bD50WnA=
|
||||
|
@ -286,6 +288,8 @@ github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI
|
|||
github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc=
|
||||
github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg=
|
||||
github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM=
|
||||
github.com/redis/go-redis/v9 v9.2.0 h1:zwMdX0A4eVzse46YN18QhuDiM4uf3JmkOB4VZrdt5uI=
|
||||
github.com/redis/go-redis/v9 v9.2.0/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
|
||||
github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
|
@ -638,8 +642,6 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0
|
|||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
||||
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/boj/redistore.v1 v1.0.0-20160128113310-fc113767cd6b h1:U/Uqd1232+wrnHOvWNaxrNqn/kFnr4yu4blgPtQt0N8=
|
||||
gopkg.in/boj/redistore.v1 v1.0.0-20160128113310-fc113767cd6b/go.mod h1:fgfIZMlsafAHpspcks2Bul+MWUNw/2dyQmjC2faKjtg=
|
||||
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=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
|
|
@ -280,7 +280,7 @@ func (a *Application) handleSignOut(rw http.ResponseWriter, r *http.Request) {
|
|||
"id_token_hint": []string{cc.RawToken},
|
||||
}
|
||||
redirect += "?" + uv.Encode()
|
||||
err = a.Logout(cc.Sub)
|
||||
err = a.Logout(r.Context(), cc.Sub)
|
||||
if err != nil {
|
||||
a.log.WithError(err).Warning("failed to logout of other sessions")
|
||||
}
|
||||
|
|
|
@ -1,23 +1,23 @@
|
|||
package application
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/garyburd/redigo/redis"
|
||||
"github.com/gorilla/securecookie"
|
||||
"github.com/gorilla/sessions"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"goauthentik.io/api/v3"
|
||||
"goauthentik.io/internal/config"
|
||||
"goauthentik.io/internal/outpost/proxyv2/codecs"
|
||||
"goauthentik.io/internal/outpost/proxyv2/constants"
|
||||
"gopkg.in/boj/redistore.v1"
|
||||
"goauthentik.io/internal/outpost/proxyv2/redisstore"
|
||||
)
|
||||
|
||||
const RedisKeyPrefix = "authentik_proxy_session_"
|
||||
|
@ -30,20 +30,26 @@ func (a *Application) getStore(p api.ProxyOutpostConfig, externalHost *url.URL)
|
|||
maxAge = int(*t) + 1
|
||||
}
|
||||
if a.isEmbedded {
|
||||
rs, err := redistore.NewRediStoreWithDB(10, "tcp", fmt.Sprintf("%s:%d", config.Get().Redis.Host, config.Get().Redis.Port), config.Get().Redis.Password, strconv.Itoa(config.Get().Redis.DB))
|
||||
client := redis.NewClient(&redis.Options{
|
||||
Addr: fmt.Sprintf("%s:%d", config.Get().Redis.Host, config.Get().Redis.Port),
|
||||
// Username: config.Get().Redis.Password,
|
||||
Password: config.Get().Redis.Password,
|
||||
DB: config.Get().Redis.DB,
|
||||
})
|
||||
|
||||
// New default RedisStore
|
||||
rs, err := redisstore.NewRedisStore(context.Background(), client)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
rs.Codecs = codecs.CodecsFromPairs(maxAge, []byte(*p.CookieSecret))
|
||||
rs.SetMaxLength(math.MaxInt)
|
||||
rs.SetKeyPrefix(RedisKeyPrefix)
|
||||
|
||||
rs.Options.HttpOnly = true
|
||||
if strings.ToLower(externalHost.Scheme) == "https" {
|
||||
rs.Options.Secure = true
|
||||
}
|
||||
rs.Options.Domain = *p.CookieDomain
|
||||
rs.Options.SameSite = http.SameSiteLaxMode
|
||||
rs.KeyPrefix(RedisKeyPrefix)
|
||||
rs.Options(sessions.Options{
|
||||
HttpOnly: strings.ToLower(externalHost.Scheme) == "https",
|
||||
Domain: *p.CookieDomain,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
})
|
||||
|
||||
a.log.Trace("using redis session backend")
|
||||
return rs
|
||||
}
|
||||
|
@ -80,7 +86,7 @@ func (a *Application) getAllCodecs() []securecookie.Codec {
|
|||
return cs
|
||||
}
|
||||
|
||||
func (a *Application) Logout(sub string) error {
|
||||
func (a *Application) Logout(ctx context.Context, sub string) error {
|
||||
if _, ok := a.sessions.(*sessions.FilesystemStore); ok {
|
||||
files, err := os.ReadDir(os.TempDir())
|
||||
if err != nil {
|
||||
|
@ -120,31 +126,22 @@ func (a *Application) Logout(sub string) error {
|
|||
}
|
||||
}
|
||||
}
|
||||
if rs, ok := a.sessions.(*redistore.RediStore); ok {
|
||||
pool := rs.Pool.Get()
|
||||
defer pool.Close()
|
||||
rep, err := pool.Do("KEYS", fmt.Sprintf("%s*", RedisKeyPrefix))
|
||||
if rs, ok := a.sessions.(*redisstore.RedisStore); ok {
|
||||
client := rs.Client()
|
||||
defer client.Close()
|
||||
keys, err := client.Keys(ctx, fmt.Sprintf("%s*", RedisKeyPrefix)).Result()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
keys, err := redis.Strings(rep, err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
serializer := redistore.GobSerializer{}
|
||||
serializer := redisstore.GobSerializer{}
|
||||
for _, key := range keys {
|
||||
v, err := pool.Do("GET", key)
|
||||
v, err := client.Get(ctx, key).Result()
|
||||
if err != nil {
|
||||
a.log.WithError(err).Warning("failed to get value")
|
||||
continue
|
||||
}
|
||||
b, err := redis.Bytes(v, err)
|
||||
if err != nil {
|
||||
a.log.WithError(err).Warning("failed to load value")
|
||||
continue
|
||||
}
|
||||
s := sessions.Session{}
|
||||
err = serializer.Deserialize(b, &s)
|
||||
err = serializer.Deserialize([]byte(v), &s)
|
||||
if err != nil {
|
||||
a.log.WithError(err).Warning("failed to deserialize")
|
||||
continue
|
||||
|
@ -156,7 +153,7 @@ func (a *Application) Logout(sub string) error {
|
|||
claims := c.(Claims)
|
||||
if claims.Sub == sub {
|
||||
a.log.WithField("key", key).Trace("deleting session")
|
||||
_, err := pool.Do("DEL", key)
|
||||
_, err := client.Del(ctx, key).Result()
|
||||
if err != nil {
|
||||
a.log.WithError(err).Warning("failed to delete key")
|
||||
continue
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2019 Ruben Cervilla
|
||||
|
||||
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.
|
|
@ -0,0 +1,200 @@
|
|||
package redisstore
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/base32"
|
||||
"encoding/gob"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/sessions"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
// RedisStore stores gorilla sessions in Redis
|
||||
type RedisStore struct {
|
||||
// client to connect to redis
|
||||
client redis.UniversalClient
|
||||
// default options to use when a new session is created
|
||||
options sessions.Options
|
||||
// key prefix with which the session will be stored
|
||||
keyPrefix string
|
||||
// key generator
|
||||
keyGen KeyGenFunc
|
||||
// session serializer
|
||||
serializer SessionSerializer
|
||||
}
|
||||
|
||||
// KeyGenFunc defines a function used by store to generate a key
|
||||
type KeyGenFunc func() (string, error)
|
||||
|
||||
// NewRedisStore returns a new RedisStore with default configuration
|
||||
func NewRedisStore(ctx context.Context, client redis.UniversalClient) (*RedisStore, error) {
|
||||
rs := &RedisStore{
|
||||
options: sessions.Options{
|
||||
Path: "/",
|
||||
MaxAge: 86400 * 30,
|
||||
},
|
||||
client: client,
|
||||
keyPrefix: "session:",
|
||||
keyGen: generateRandomKey,
|
||||
serializer: GobSerializer{},
|
||||
}
|
||||
|
||||
return rs, rs.client.Ping(ctx).Err()
|
||||
}
|
||||
|
||||
func (s *RedisStore) Client() redis.UniversalClient {
|
||||
return s.client
|
||||
}
|
||||
|
||||
// Get returns a session for the given name after adding it to the registry.
|
||||
func (s *RedisStore) Get(r *http.Request, name string) (*sessions.Session, error) {
|
||||
return sessions.GetRegistry(r).Get(s, name)
|
||||
}
|
||||
|
||||
// New returns a session for the given name without adding it to the registry.
|
||||
func (s *RedisStore) New(r *http.Request, name string) (*sessions.Session, error) {
|
||||
session := sessions.NewSession(s, name)
|
||||
opts := s.options
|
||||
session.Options = &opts
|
||||
session.IsNew = true
|
||||
|
||||
c, err := r.Cookie(name)
|
||||
if err != nil {
|
||||
return session, nil
|
||||
}
|
||||
session.ID = c.Value
|
||||
|
||||
err = s.load(r.Context(), session)
|
||||
if err == nil {
|
||||
session.IsNew = false
|
||||
} else if err == redis.Nil {
|
||||
err = nil // no data stored
|
||||
}
|
||||
return session, err
|
||||
}
|
||||
|
||||
// Save adds a single session to the response.
|
||||
//
|
||||
// If the Options.MaxAge of the session is <= 0 then the session file will be
|
||||
// deleted from the store. With this process it enforces the properly
|
||||
// session cookie handling so no need to trust in the cookie management in the
|
||||
// web browser.
|
||||
func (s *RedisStore) Save(r *http.Request, w http.ResponseWriter, session *sessions.Session) error {
|
||||
// Delete if max-age is <= 0
|
||||
if session.Options.MaxAge <= 0 {
|
||||
if err := s.delete(r.Context(), session); err != nil {
|
||||
return err
|
||||
}
|
||||
http.SetCookie(w, sessions.NewCookie(session.Name(), "", session.Options))
|
||||
return nil
|
||||
}
|
||||
|
||||
if session.ID == "" {
|
||||
id, err := s.keyGen()
|
||||
if err != nil {
|
||||
return errors.New("redisstore: failed to generate session id")
|
||||
}
|
||||
session.ID = id
|
||||
}
|
||||
if err := s.save(r.Context(), session); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
http.SetCookie(w, sessions.NewCookie(session.Name(), session.ID, session.Options))
|
||||
return nil
|
||||
}
|
||||
|
||||
// Options set options to use when a new session is created
|
||||
func (s *RedisStore) Options(opts sessions.Options) {
|
||||
s.options = opts
|
||||
}
|
||||
|
||||
// KeyPrefix sets the key prefix to store session in Redis
|
||||
func (s *RedisStore) KeyPrefix(keyPrefix string) {
|
||||
s.keyPrefix = keyPrefix
|
||||
}
|
||||
|
||||
// KeyGen sets the key generator function
|
||||
func (s *RedisStore) KeyGen(f KeyGenFunc) {
|
||||
s.keyGen = f
|
||||
}
|
||||
|
||||
// Serializer sets the session serializer to store session
|
||||
func (s *RedisStore) Serializer(ss SessionSerializer) {
|
||||
s.serializer = ss
|
||||
}
|
||||
|
||||
// Close closes the Redis store
|
||||
func (s *RedisStore) Close() error {
|
||||
return s.client.Close()
|
||||
}
|
||||
|
||||
// save writes session in Redis
|
||||
func (s *RedisStore) save(ctx context.Context, session *sessions.Session) error {
|
||||
b, err := s.serializer.Serialize(session)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.client.Set(ctx, s.keyPrefix+session.ID, b, time.Duration(session.Options.MaxAge)*time.Second).Err()
|
||||
}
|
||||
|
||||
// load reads session from Redis
|
||||
func (s *RedisStore) load(ctx context.Context, session *sessions.Session) error {
|
||||
cmd := s.client.Get(ctx, s.keyPrefix+session.ID)
|
||||
if cmd.Err() != nil {
|
||||
return cmd.Err()
|
||||
}
|
||||
|
||||
b, err := cmd.Bytes()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.serializer.Deserialize(b, session)
|
||||
}
|
||||
|
||||
// delete deletes session in Redis
|
||||
func (s *RedisStore) delete(ctx context.Context, session *sessions.Session) error {
|
||||
return s.client.Del(ctx, s.keyPrefix+session.ID).Err()
|
||||
}
|
||||
|
||||
// SessionSerializer provides an interface for serialize/deserialize a session
|
||||
type SessionSerializer interface {
|
||||
Serialize(s *sessions.Session) ([]byte, error)
|
||||
Deserialize(b []byte, s *sessions.Session) error
|
||||
}
|
||||
|
||||
// Gob serializer
|
||||
type GobSerializer struct{}
|
||||
|
||||
func (gs GobSerializer) Serialize(s *sessions.Session) ([]byte, error) {
|
||||
buf := new(bytes.Buffer)
|
||||
enc := gob.NewEncoder(buf)
|
||||
err := enc.Encode(s.Values)
|
||||
if err == nil {
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func (gs GobSerializer) Deserialize(d []byte, s *sessions.Session) error {
|
||||
dec := gob.NewDecoder(bytes.NewBuffer(d))
|
||||
return dec.Decode(&s.Values)
|
||||
}
|
||||
|
||||
// generateRandomKey returns a new random key
|
||||
func generateRandomKey() (string, error) {
|
||||
k := make([]byte, 64)
|
||||
if _, err := io.ReadFull(rand.Reader, k); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.TrimRight(base32.StdEncoding.EncodeToString(k), "="), nil
|
||||
}
|
|
@ -0,0 +1,158 @@
|
|||
package redisstore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gorilla/sessions"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
const (
|
||||
redisAddr = "localhost:6379"
|
||||
)
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
client := redis.NewClient(&redis.Options{
|
||||
Addr: redisAddr,
|
||||
})
|
||||
|
||||
store, err := NewRedisStore(context.Background(), client)
|
||||
if err != nil {
|
||||
t.Fatal("failed to create redis store", err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", "http://www.example.com", nil)
|
||||
if err != nil {
|
||||
t.Fatal("failed to create request", err)
|
||||
}
|
||||
|
||||
session, err := store.New(req, "hello")
|
||||
if err != nil {
|
||||
t.Fatal("failed to create session", err)
|
||||
}
|
||||
if session.IsNew == false {
|
||||
t.Fatal("session is not new")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOptions(t *testing.T) {
|
||||
client := redis.NewClient(&redis.Options{
|
||||
Addr: redisAddr,
|
||||
})
|
||||
|
||||
store, err := NewRedisStore(context.Background(), client)
|
||||
if err != nil {
|
||||
t.Fatal("failed to create redis store", err)
|
||||
}
|
||||
|
||||
opts := sessions.Options{
|
||||
Path: "/path",
|
||||
MaxAge: 99999,
|
||||
}
|
||||
store.Options(opts)
|
||||
|
||||
req, err := http.NewRequest("GET", "http://www.example.com", nil)
|
||||
if err != nil {
|
||||
t.Fatal("failed to create request", err)
|
||||
}
|
||||
|
||||
session, err := store.New(req, "hello")
|
||||
if err != nil {
|
||||
t.Fatal("failed to create store", err)
|
||||
}
|
||||
if session.Options.Path != opts.Path || session.Options.MaxAge != opts.MaxAge {
|
||||
t.Fatal("failed to set options")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSave(t *testing.T) {
|
||||
client := redis.NewClient(&redis.Options{
|
||||
Addr: redisAddr,
|
||||
})
|
||||
|
||||
store, err := NewRedisStore(context.Background(), client)
|
||||
if err != nil {
|
||||
t.Fatal("failed to create redis store", err)
|
||||
}
|
||||
|
||||
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, "hello")
|
||||
if err != nil {
|
||||
t.Fatal("failed to create session", err)
|
||||
}
|
||||
|
||||
session.Values["key"] = "value"
|
||||
err = session.Save(req, w)
|
||||
if err != nil {
|
||||
t.Fatal("failed to save: ", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDelete(t *testing.T) {
|
||||
client := redis.NewClient(&redis.Options{
|
||||
Addr: redisAddr,
|
||||
})
|
||||
|
||||
store, err := NewRedisStore(context.Background(), client)
|
||||
if err != nil {
|
||||
t.Fatal("failed to create redis store", err)
|
||||
}
|
||||
|
||||
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, "hello")
|
||||
if err != nil {
|
||||
t.Fatal("failed to create session", err)
|
||||
}
|
||||
|
||||
session.Values["key"] = "value"
|
||||
err = session.Save(req, w)
|
||||
if err != nil {
|
||||
t.Fatal("failed to save session: ", err)
|
||||
}
|
||||
|
||||
session.Options.MaxAge = -1
|
||||
err = session.Save(req, w)
|
||||
if err != nil {
|
||||
t.Fatal("failed to delete session: ", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClose(t *testing.T) {
|
||||
client := redis.NewClient(&redis.Options{
|
||||
Addr: redisAddr,
|
||||
})
|
||||
|
||||
cmd := client.Ping(context.Background())
|
||||
err := cmd.Err()
|
||||
if err != nil {
|
||||
t.Fatal("connection is not opened")
|
||||
}
|
||||
|
||||
store, err := NewRedisStore(context.Background(), client)
|
||||
if err != nil {
|
||||
t.Fatal("failed to create redis store", err)
|
||||
}
|
||||
|
||||
err = store.Close()
|
||||
if err != nil {
|
||||
t.Fatal("failed to close")
|
||||
}
|
||||
|
||||
cmd = client.Ping(context.Background())
|
||||
if cmd.Err() == nil {
|
||||
t.Fatal("connection is properly closed")
|
||||
}
|
||||
}
|
Reference in New Issue