mirror of
https://github.com/go-gitea/gitea.git
synced 2026-10-03 06:10:49 +00:00
refactor!: move go-chi/session into Gitea (#39504)
The `gitea.com/go-chi/session` package only exists for Gitea, so it moves into `modules/session` to fix its bugs directly. Fixes the flake in https://github.com/go-gitea/gitea/actions/runs/36726154500/job/109923538400. - Sessions are only written back when changed, so a read-only request can't revert a concurrent change or restore a logged-out session, like https://github.com/go-macaron/session/commit/ae808a4a4660c802965c834299ab08f167effd12 - The session cookie is only set once a session holds data - Every backend refreshes the expiry on load and file sessions are written atomically - Also fix https://github.com/go-gitea/gitea/issues/36176 ## ⚠️ BREAKING ⚠️ * the `mysql`, `postgres`, `couchbase` and `memcache` session providers are removed, use `file`, `db` or `redis` instead * login-related cookies are renamed to `gitea_session` and `gitea_remember`, if you'd like to use the old names, set `COOKIE_NAME` and `COOKIE_REMEMBER_NAME` in app.ini --------- Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
Generated
-25
File diff suppressed because one or more lines are too long
+2
-8
@@ -13,13 +13,11 @@ import (
|
||||
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/modules/dump"
|
||||
"gitea.dev/modules/json"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/storage"
|
||||
"gitea.dev/modules/util"
|
||||
|
||||
"gitea.com/go-chi/session"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
@@ -249,12 +247,8 @@ func runDump(ctx context.Context, cmd *cli.Command) error {
|
||||
log.Info("Packing data directory...%s", setting.AppDataPath)
|
||||
|
||||
var excludes []string
|
||||
if setting.SessionConfig.OriginalProvider == "file" {
|
||||
var opts session.Options
|
||||
if err = json.Unmarshal([]byte(setting.SessionConfig.ProviderConfig), &opts); err != nil {
|
||||
return err
|
||||
}
|
||||
excludes = append(excludes, opts.ProviderConfig)
|
||||
if setting.SessionConfig.Provider == "file" {
|
||||
excludes = append(excludes, setting.SessionConfig.ProviderConfig)
|
||||
}
|
||||
|
||||
if cmd.IsSet("skip-index") && cmd.Bool("skip-index") {
|
||||
|
||||
@@ -459,7 +459,7 @@ INTERNAL_TOKEN =
|
||||
;LOGIN_REMEMBER_DAYS = 31
|
||||
;;
|
||||
;; Name of cookie used to store authentication information.
|
||||
;COOKIE_REMEMBER_NAME = gitea_incredible
|
||||
;COOKIE_REMEMBER_NAME = gitea_remember
|
||||
;;
|
||||
;; URL or path that Gitea should redirect users to *after* performing its own logout.
|
||||
;; Use this, if needed, when authentication is handled by a reverse proxy or SSO.
|
||||
@@ -2012,19 +2012,17 @@ LEVEL = Info
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;;
|
||||
;; Either "memory", "file", "redis", "db", "mysql", "couchbase", "memcache" or "postgres"
|
||||
;; Default is "file". "db" will reuse the configuration in [database]
|
||||
;; Either "memory", "file", "redis" or "db", default is "file". "db" will reuse the configuration in [database]
|
||||
;PROVIDER = file
|
||||
;;
|
||||
;; Provider config options
|
||||
;; memory: doesn't have any config yet
|
||||
;; file: session file path, e.g. `data/sessions`, relative paths will be made absolute against _`AppWorkPath`_.
|
||||
;; redis: default to [redis] CONN_STR
|
||||
;; mysql: go-sql-driver/mysql dsn config string, e.g. `root:password@/session_table`
|
||||
;PROVIDER_CONFIG =
|
||||
;;
|
||||
;; Session cookie name
|
||||
;COOKIE_NAME = i_like_gitea
|
||||
;COOKIE_NAME = gitea_session
|
||||
;;
|
||||
;; If you use session in https only: true or false. If not set, it defaults to `true` if the ROOT_URL is an HTTPS URL.
|
||||
;COOKIE_SECURE =
|
||||
|
||||
@@ -6,7 +6,6 @@ toolchain go1.27.1
|
||||
|
||||
require (
|
||||
connectrpc.com/connect v1.21.0
|
||||
gitea.com/go-chi/session v0.0.0-20260708011333-ebced8a7a2d6
|
||||
gitea.com/lunny/dingtalk_webhook v0.0.0-20171025031554-e3534c89ef96
|
||||
gitea.com/lunny/levelqueue v0.4.2-0.20230414023320-3c0159fe0fe4
|
||||
gitea.dev/actionslib v1.2.1
|
||||
@@ -152,9 +151,6 @@ require (
|
||||
github.com/clipperhouse/displaywidth v0.11.0 // indirect
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
||||
github.com/cloudflare/circl v1.6.3 // indirect
|
||||
github.com/couchbase/go-couchbase v0.1.1 // indirect
|
||||
github.com/couchbase/gomemcached v0.3.4 // indirect
|
||||
github.com/couchbase/goutils v0.3.0 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
@@ -175,7 +171,6 @@ require (
|
||||
github.com/golang-sql/sqlexp v0.1.0 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||
github.com/golang/snappy v1.0.0 // indirect
|
||||
github.com/google/flatbuffers v25.12.19+incompatible // indirect
|
||||
github.com/google/go-querystring v1.2.0 // indirect
|
||||
github.com/google/go-tpm v0.9.8 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
|
||||
@@ -8,8 +8,6 @@ dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
||||
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
|
||||
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
|
||||
gitea.com/go-chi/session v0.0.0-20260708011333-ebced8a7a2d6 h1:YWzVGeC/8SZThrJS48ZmQYLkzssdeABxHPhbdnxPDIU=
|
||||
gitea.com/go-chi/session v0.0.0-20260708011333-ebced8a7a2d6/go.mod h1:KDvcfMUoXfATPHs2mbMoXFTXT45/FAFAS39waz9tPk0=
|
||||
gitea.com/lunny/dingtalk_webhook v0.0.0-20171025031554-e3534c89ef96 h1:+wWBi6Qfruqu7xJgjOIrKVQGiLUZdpKYCZewJ4clqhw=
|
||||
gitea.com/lunny/dingtalk_webhook v0.0.0-20171025031554-e3534c89ef96/go.mod h1:VyMQP6ue6MKHM8UsOXfNfuMKD0oSAWZdXVcpHIN2yaY=
|
||||
gitea.com/lunny/levelqueue v0.4.2-0.20230414023320-3c0159fe0fe4 h1:IFT+hup2xejHqdhS7keYWioqfmxdnfblFDTGoOwcZ+o=
|
||||
@@ -167,13 +165,6 @@ github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg
|
||||
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
|
||||
github.com/coder/websocket v1.8.15 h1:6B2JPeOGlpff2Uz6vOEH1Vzpi0iUz20A+lPVhPHtNUA=
|
||||
github.com/coder/websocket v1.8.15/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
|
||||
github.com/couchbase/go-couchbase v0.1.1 h1:ClFXELcKj/ojyoTYbsY34QUrrYCBi/1G749sXSCkdhk=
|
||||
github.com/couchbase/go-couchbase v0.1.1/go.mod h1:+/bddYDxXsf9qt0xpDUtRR47A2GjaXmGGAqQ/k3GJ8A=
|
||||
github.com/couchbase/gomemcached v0.3.4 h1:VGdrZUJbt5lLyI/MXnyVCZKHKYXg/vaud08lJIAeZps=
|
||||
github.com/couchbase/gomemcached v0.3.4/go.mod h1:pISAjweI42vljCumsJIo7CVhqIMIIP9g3Wfhl1JJw68=
|
||||
github.com/couchbase/goutils v0.1.2/go.mod h1:h89Ek/tiOxxqjz30nPPlwZdQbdB8BwgnuBxeoUe/ViE=
|
||||
github.com/couchbase/goutils v0.3.0 h1:rsv72B6BDjW9jmwlfiDUrdu3EpNvPuo5WLULHzQ0DLE=
|
||||
github.com/couchbase/goutils v0.3.0/go.mod h1:7Gm+D3vXfV4HS+hQWvKfy6e6ILCptGXNqBKvQXhplhk=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE=
|
||||
@@ -308,9 +299,6 @@ github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
|
||||
github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/gomodule/redigo v1.9.3 h1:dNPSXeXv6HCq2jdyWfjgmhBdqnR6PRO3m/G05nvpPC8=
|
||||
github.com/gomodule/redigo v1.9.3/go.mod h1:KsU3hiK/Ay8U42qpaJk+kuNa3C+spxapWpM+ywhcgtw=
|
||||
github.com/google/flatbuffers v24.3.25+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
|
||||
github.com/google/flatbuffers v25.12.19+incompatible h1:haMV2JRRJCe1998HeW/p0X9UaMTK6SDo0ffLn2+DbLs=
|
||||
github.com/google/flatbuffers v25.12.19+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
@@ -337,8 +325,6 @@ github.com/google/pprof v0.0.0-20260906184651-6331bc6350fe h1:QAinXoAFJdGQYztXn3
|
||||
github.com/google/pprof v0.0.0-20260906184651-6331bc6350fe/go.mod h1:jl5iWTm0/hd5PjEYEOuwAJ57L/CibdZfrqZ5XA5GrCk=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e h1:JKmoR8x90Iww1ks85zJ1lfDGgIiMDuIptTOhJq+zKyg=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/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/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
|
||||
@@ -396,8 +382,6 @@ github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7X
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
||||
github.com/kevinburke/ssh_config v1.6.0 h1:J1FBfmuVosPHf5GRdltRLhPJtJpTlMdKTBjRgTaQBFY=
|
||||
@@ -571,10 +555,6 @@ github.com/sirupsen/logrus v1.10.2 h1:G2SED73/qrAu6YwbdxOD6peLkCBI3z7L+ykJFTXJBB
|
||||
github.com/sirupsen/logrus v1.10.2/go.mod h1:SLEg8TqYulVKKfIGHldVp2K2aYz2DKSVBq4g/H5bR7Q=
|
||||
github.com/skeema/knownhosts v1.3.2 h1:EDL9mgf4NzwMXCTfaxSD/o/a5fxDw/xL9nkU28JjdBg=
|
||||
github.com/skeema/knownhosts v1.3.2/go.mod h1:bEg3iQAuw+jyiw+484wwFJoKSLwcfd7fqRy+N0QTiow=
|
||||
github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304 h1:Jpy1PXuP99tXNrhbq2BaPz9B+jNAvH1JPQQpG/9GCXY=
|
||||
github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337 h1:WN9BUFbdyOsSH/XohnWpXOlq9NBD5sGAB2FciQMUEe8=
|
||||
github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/sorairolake/lzip-go v0.3.8 h1:j5Q2313INdTA80ureWYRhX+1K78mUXfMoPZCw/ivWik=
|
||||
github.com/sorairolake/lzip-go v0.3.8/go.mod h1:JcBqGMV0frlxwrsE9sMWXDjqn3EeVf0/54YPsw66qkU=
|
||||
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||
@@ -598,7 +578,6 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
|
||||
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
|
||||
@@ -672,10 +651,6 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
|
||||
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
|
||||
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
|
||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
|
||||
@@ -684,10 +659,6 @@ golang.org/x/image v0.46.0 h1:b1+oYj0Jbp6K5MDT4i4/eZpYlk3V8SJhhDKh6LBHAyQ=
|
||||
golang.org/x/image v0.46.0/go.mod h1:3B3W05VGVQyuXucLINLjXKrqISASfi4Xj+iCVkLMwew=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c=
|
||||
golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -698,12 +669,7 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.59.0 h1:5zfYln+w5XCxwrnMMJPufRgNoXEaGxl0wo5GqPXyues=
|
||||
golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
|
||||
golang.org/x/oauth2 v0.37.0 h1:JUlcxA8oAtauLfiH8FX2/FkAWHAdi0QtGCGc+hofE98=
|
||||
@@ -713,11 +679,6 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk=
|
||||
golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -741,22 +702,11 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
|
||||
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
|
||||
golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE=
|
||||
golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@@ -764,11 +714,6 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI=
|
||||
golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=
|
||||
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||
@@ -777,9 +722,6 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI=
|
||||
golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
||||
+29
-72
@@ -5,7 +5,6 @@ package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/modules/timeutil"
|
||||
@@ -13,49 +12,46 @@ import (
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
// Session represents a session compatible for go-chi session
|
||||
type Session struct {
|
||||
Key string `xorm:"pk CHAR(16)"` // has to be Key to match with go-chi/session
|
||||
Data []byte `xorm:"BLOB"` // on MySQL this has a maximum size of 64Kb - this may need to be increased
|
||||
Expiry timeutil.TimeStamp // has to be Expiry to match with go-chi/session
|
||||
Key string `xorm:"pk CHAR(16)"` // the limit is from legacy go-chi/session
|
||||
Data []byte `xorm:"BLOB"` // on MySQL this has a maximum size of 64Kb
|
||||
LastAccessTime timeutil.TimeStamp `xorm:"expiry"` // last access time, the field name is from legacy go-chi/session, we don't want to change it at the moment
|
||||
}
|
||||
|
||||
const DbSessionLastAccessTime = "expiry" // maybe we can make a deeper clean up in the future, just keep this PR focused
|
||||
|
||||
func init() {
|
||||
db.RegisterModel(new(Session))
|
||||
}
|
||||
|
||||
// UpdateSession updates the session with provided id
|
||||
func UpdateSession(ctx context.Context, key string, data []byte) error {
|
||||
_, err := db.GetEngine(ctx).ID(key).Update(&Session{
|
||||
Data: data,
|
||||
Expiry: timeutil.TimeStampNow(),
|
||||
})
|
||||
// UpdateSession stores the data of the session with provided id, creating the session only if create is set
|
||||
func UpdateSession(ctx context.Context, key string, data []byte, create bool) error {
|
||||
session := &Session{Key: key, Data: data, LastAccessTime: timeutil.TimeStampNow()}
|
||||
update := func() (int64, error) {
|
||||
return db.GetEngine(ctx).ID(key).Cols("data", DbSessionLastAccessTime).Update(session)
|
||||
}
|
||||
if updated, err := update(); err != nil || updated > 0 || !create {
|
||||
return err
|
||||
}
|
||||
insertErr := db.Insert(ctx, session)
|
||||
if insertErr == nil {
|
||||
return nil
|
||||
}
|
||||
// the row exists if a concurrent request inserted it, or if MySQL reported an unchanged row as not updated
|
||||
if exist, err := db.Exist[Session](ctx, builder.Eq{"`key`": key}); err != nil || !exist {
|
||||
return insertErr
|
||||
}
|
||||
_, err := update()
|
||||
return err
|
||||
}
|
||||
|
||||
// ReadSession reads the data for the provided session
|
||||
func ReadSession(ctx context.Context, key string) (*Session, error) {
|
||||
return db.WithTx2(ctx, func(ctx context.Context) (*Session, error) {
|
||||
session, exist, err := db.Get[Session](ctx, builder.Eq{"`key`": key})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !exist {
|
||||
session = &Session{
|
||||
Key: key,
|
||||
Expiry: timeutil.TimeStampNow(),
|
||||
}
|
||||
if err := db.Insert(ctx, session); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return session, nil
|
||||
})
|
||||
func UpdateSessionLastAccessTime(ctx context.Context, key string) error {
|
||||
_, err := db.GetEngine(ctx).ID(key).Cols(DbSessionLastAccessTime).Update(&Session{LastAccessTime: timeutil.TimeStampNow()})
|
||||
return err
|
||||
}
|
||||
|
||||
// ExistSession checks if a session exists
|
||||
func ExistSession(ctx context.Context, key string) (bool, error) {
|
||||
return db.Exist[Session](ctx, builder.Eq{"`key`": key})
|
||||
func GetSession(ctx context.Context, key string) (*Session, bool, error) {
|
||||
return db.Get[Session](ctx, builder.Eq{"`key`": key})
|
||||
}
|
||||
|
||||
// DestroySession destroys a session
|
||||
@@ -66,47 +62,8 @@ func DestroySession(ctx context.Context, key string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// RegenerateSession regenerates a session from the old id
|
||||
func RegenerateSession(ctx context.Context, oldKey, newKey string) (*Session, error) {
|
||||
return db.WithTx2(ctx, func(ctx context.Context) (*Session, error) {
|
||||
if has, err := db.Exist[Session](ctx, builder.Eq{"`key`": newKey}); err != nil {
|
||||
return nil, err
|
||||
} else if has {
|
||||
return nil, fmt.Errorf("session Key: %s already exists", newKey)
|
||||
}
|
||||
|
||||
if has, err := db.Exist[Session](ctx, builder.Eq{"`key`": oldKey}); err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
if err := db.Insert(ctx, &Session{
|
||||
Key: oldKey,
|
||||
Expiry: timeutil.TimeStampNow(),
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := db.Exec(ctx, "UPDATE `session` SET `key` = ? WHERE `key`=?", newKey, oldKey); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s, _, err := db.Get[Session](ctx, builder.Eq{"`key`": newKey})
|
||||
if err != nil {
|
||||
// is not exist, it should be impossible
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s, nil
|
||||
})
|
||||
}
|
||||
|
||||
// CountSessions returns the number of sessions
|
||||
func CountSessions(ctx context.Context) (int64, error) {
|
||||
return db.GetEngine(ctx).Count(&Session{})
|
||||
}
|
||||
|
||||
// CleanupSessions cleans up expired sessions
|
||||
func CleanupSessions(ctx context.Context, maxLifetime int64) error {
|
||||
_, err := db.GetEngine(ctx).Where("expiry <= ?", timeutil.TimeStampNow().Add(-maxLifetime)).Delete(&Session{})
|
||||
_, err := db.GetEngine(ctx).Where(DbSessionLastAccessTime+" <= ?", timeutil.TimeStampNow().Add(-maxLifetime)).Delete(&Session{})
|
||||
return err
|
||||
}
|
||||
|
||||
+13
-147
@@ -5,172 +5,38 @@ package session
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"sync"
|
||||
|
||||
"gitea.dev/models/auth"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/timeutil"
|
||||
|
||||
"gitea.com/go-chi/session"
|
||||
)
|
||||
|
||||
// DBStore represents a session store implementation based on the DB.
|
||||
type DBStore struct {
|
||||
sid string
|
||||
lock sync.RWMutex
|
||||
data map[any]any
|
||||
type dbBackend struct {
|
||||
maxLifetime int64
|
||||
}
|
||||
|
||||
func dbContext() context.Context {
|
||||
return context.Background()
|
||||
}
|
||||
|
||||
// NewDBStore creates and returns a DB session store.
|
||||
func NewDBStore(sid string, kv map[any]any) *DBStore {
|
||||
return &DBStore{
|
||||
sid: sid,
|
||||
data: kv,
|
||||
}
|
||||
}
|
||||
|
||||
// Set sets value to given key in session.
|
||||
func (s *DBStore) Set(key, val any) error {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
s.data[key] = val
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get gets value by given key in session.
|
||||
func (s *DBStore) Get(key any) any {
|
||||
s.lock.RLock()
|
||||
defer s.lock.RUnlock()
|
||||
|
||||
return s.data[key]
|
||||
}
|
||||
|
||||
// Delete delete a key from session.
|
||||
func (s *DBStore) Delete(key any) error {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
delete(s.data, key)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ID returns current session ID.
|
||||
func (s *DBStore) ID() string {
|
||||
return s.sid
|
||||
}
|
||||
|
||||
// Release releases resource and save data to provider.
|
||||
func (s *DBStore) Release() error {
|
||||
// Skip encoding if the data is empty
|
||||
if len(s.data) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
data, err := session.EncodeGob(s.data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return auth.UpdateSession(dbContext(), s.sid, data)
|
||||
}
|
||||
|
||||
// Flush deletes all session data.
|
||||
func (s *DBStore) Flush() error {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
s.data = make(map[any]any)
|
||||
return nil
|
||||
}
|
||||
|
||||
// DBProvider represents a DB session provider implementation.
|
||||
type DBProvider struct {
|
||||
maxLifetime int64
|
||||
}
|
||||
|
||||
// Init initializes DB session provider.
|
||||
// connStr: username:password@protocol(address)/dbname?param=value
|
||||
func (p *DBProvider) Init(maxLifetime int64, connStr string) error {
|
||||
p.maxLifetime = maxLifetime
|
||||
return nil
|
||||
}
|
||||
|
||||
// Read returns raw session store by session ID.
|
||||
func (p *DBProvider) Read(sid string) (session.RawStore, error) {
|
||||
s, err := auth.ReadSession(dbContext(), sid)
|
||||
if err != nil {
|
||||
func (b *dbBackend) load(sid string) ([]byte, error) {
|
||||
sess, exist, err := auth.GetSession(dbContext(), sid)
|
||||
if err != nil || !exist || sess.LastAccessTime.Add(b.maxLifetime) <= timeutil.TimeStampNow() {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var kv map[any]any
|
||||
if len(s.Data) == 0 || s.Expiry.Add(p.maxLifetime) <= timeutil.TimeStampNow() {
|
||||
kv = make(map[any]any)
|
||||
} else {
|
||||
kv, err = session.DecodeGob(s.Data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return NewDBStore(sid, kv), nil
|
||||
return sess.Data, auth.UpdateSessionLastAccessTime(dbContext(), sid)
|
||||
}
|
||||
|
||||
// Exist returns true if session with given ID exists.
|
||||
func (p *DBProvider) Exist(sid string) (bool, error) {
|
||||
has, err := auth.ExistSession(dbContext(), sid)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("session/DB: error checking existence: %w", err)
|
||||
}
|
||||
return has, nil
|
||||
func (b *dbBackend) save(sid string, data []byte, create bool) error {
|
||||
return auth.UpdateSession(dbContext(), sid, data, create)
|
||||
}
|
||||
|
||||
// Destroy deletes a session by session ID.
|
||||
func (p *DBProvider) Destroy(sid string) error {
|
||||
func (b *dbBackend) destroy(sid string) error {
|
||||
return auth.DestroySession(dbContext(), sid)
|
||||
}
|
||||
|
||||
// Regenerate regenerates a session store from old session ID to new one.
|
||||
func (p *DBProvider) Regenerate(oldsid, sid string) (_ session.RawStore, err error) {
|
||||
s, err := auth.RegenerateSession(dbContext(), oldsid, sid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var kv map[any]any
|
||||
if len(s.Data) == 0 || s.Expiry.Add(p.maxLifetime) <= timeutil.TimeStampNow() {
|
||||
kv = make(map[any]any)
|
||||
} else {
|
||||
kv, err = session.DecodeGob(s.Data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return NewDBStore(sid, kv), nil
|
||||
}
|
||||
|
||||
// Count counts and returns number of sessions.
|
||||
func (p *DBProvider) Count() (int, error) {
|
||||
total, err := auth.CountSessions(dbContext())
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("session/DB: error counting records: %w", err)
|
||||
}
|
||||
return int(total), nil
|
||||
}
|
||||
|
||||
// GC calls GC to clean expired sessions.
|
||||
func (p *DBProvider) GC() {
|
||||
if err := auth.CleanupSessions(dbContext(), p.maxLifetime); err != nil {
|
||||
log.Printf("session/DB: error garbage collecting: %v", err)
|
||||
func (b *dbBackend) gc() {
|
||||
if err := auth.CleanupSessions(dbContext(), b.maxLifetime); err != nil {
|
||||
log.Error("Unable to garbage collect sessions: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
session.Register("db", &DBProvider{})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
// Copyright 2013 Beego Authors
|
||||
// Copyright 2014 The Macaron Authors
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package session
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/util"
|
||||
)
|
||||
|
||||
type fileBackend struct {
|
||||
lock sync.RWMutex // exclusive for removals, so they never interleave with a load or save
|
||||
rootPath string
|
||||
maxLifetime time.Duration
|
||||
}
|
||||
|
||||
func newFileBackend(rootPath string, maxLifetime int64) *fileBackend {
|
||||
return &fileBackend{rootPath: filepath.Clean(rootPath), maxLifetime: time.Duration(maxLifetime) * time.Second}
|
||||
}
|
||||
|
||||
func (b *fileBackend) filepath(sid string) string {
|
||||
return filepath.Join(b.rootPath, sid[0:1], sid[1:2], sid)
|
||||
}
|
||||
|
||||
func ignoreNotExist(err error) error {
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (b *fileBackend) load(sid string) ([]byte, error) {
|
||||
b.lock.RLock()
|
||||
defer b.lock.RUnlock()
|
||||
filename := b.filepath(sid)
|
||||
stat, err := os.Lstat(filename)
|
||||
if err != nil {
|
||||
return nil, ignoreNotExist(err)
|
||||
}
|
||||
if !stat.Mode().IsRegular() {
|
||||
return nil, fmt.Errorf("session file %s is not a regular file", filename)
|
||||
}
|
||||
if time.Since(stat.ModTime()) > b.maxLifetime {
|
||||
return nil, nil
|
||||
}
|
||||
data, err := os.ReadFile(filename)
|
||||
if err == nil {
|
||||
now := time.Now()
|
||||
err = os.Chtimes(filename, now, now)
|
||||
}
|
||||
return data, ignoreNotExist(err)
|
||||
}
|
||||
|
||||
func (b *fileBackend) save(sid string, data []byte, create bool) error {
|
||||
b.lock.RLock()
|
||||
defer b.lock.RUnlock()
|
||||
filename := b.filepath(sid)
|
||||
if create {
|
||||
if err := os.MkdirAll(filepath.Dir(filename), 0o700); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if _, err := os.Lstat(filename); err != nil {
|
||||
return ignoreNotExist(err)
|
||||
}
|
||||
tmpFile, err := os.CreateTemp(filepath.Dir(filename), sid+".*.tmp")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = tmpFile.Write(data)
|
||||
if err = errors.Join(err, tmpFile.Close()); err == nil {
|
||||
err = util.RenameWithRetry(tmpFile.Name(), filename)
|
||||
}
|
||||
if err != nil {
|
||||
_ = os.Remove(tmpFile.Name())
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (b *fileBackend) destroy(sid string) error {
|
||||
b.lock.Lock()
|
||||
defer b.lock.Unlock()
|
||||
return ignoreNotExist(os.Remove(b.filepath(sid)))
|
||||
}
|
||||
|
||||
func (b *fileBackend) expired(path string) bool {
|
||||
info, err := os.Lstat(path)
|
||||
return err == nil && time.Since(info.ModTime()) > b.maxLifetime
|
||||
}
|
||||
|
||||
func (b *fileBackend) gc() {
|
||||
err := filepath.WalkDir(b.rootPath, func(path string, entry fs.DirEntry, err error) error {
|
||||
if err != nil || entry.IsDir() || !b.expired(path) {
|
||||
return ignoreNotExist(err)
|
||||
}
|
||||
b.lock.Lock()
|
||||
defer b.lock.Unlock()
|
||||
if b.expired(path) { // a concurrent load may have refreshed it
|
||||
err = os.Remove(path)
|
||||
}
|
||||
return ignoreNotExist(err)
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("Unable to garbage collect session files: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package session
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gitea.dev/models/unittest"
|
||||
|
||||
_ "gitea.dev/models"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
unittest.MainTest(m, &unittest.TestOptions{FixtureFiles: []string{}})
|
||||
}
|
||||
+84
-36
@@ -4,65 +4,113 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"maps"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"gitea.com/go-chi/session"
|
||||
"gitea.dev/modules/util"
|
||||
)
|
||||
|
||||
type mockMemRawStore struct {
|
||||
s *session.MemStore
|
||||
type memoryBackend struct {
|
||||
lock sync.Mutex
|
||||
maxLifetime time.Duration
|
||||
sessions map[string]memorySession
|
||||
}
|
||||
|
||||
var _ session.RawStore = (*mockMemRawStore)(nil)
|
||||
type memorySession struct {
|
||||
data []byte
|
||||
accessed time.Time
|
||||
}
|
||||
|
||||
func (m *mockMemRawStore) Set(k, v any) error {
|
||||
// We need to use gob to encode the value, to make it have the same behavior as other stores and catch abuses.
|
||||
// Because gob needs to "Register" the type before it can encode it, and it's unable to decode a struct to "any" so use a map to help to decode the value.
|
||||
var buf bytes.Buffer
|
||||
if err := gob.NewEncoder(&buf).Encode(map[string]any{"v": v}); err != nil {
|
||||
return err
|
||||
func newMemoryBackend(maxLifetime int64) *memoryBackend {
|
||||
return &memoryBackend{maxLifetime: time.Duration(maxLifetime) * time.Second, sessions: map[string]memorySession{}}
|
||||
}
|
||||
|
||||
func (b *memoryBackend) expired(sess memorySession) bool {
|
||||
return time.Since(sess.accessed) > b.maxLifetime
|
||||
}
|
||||
|
||||
func (b *memoryBackend) load(sid string) ([]byte, error) {
|
||||
b.lock.Lock()
|
||||
defer b.lock.Unlock()
|
||||
sess, ok := b.sessions[sid]
|
||||
if !ok || b.expired(sess) {
|
||||
return nil, nil
|
||||
}
|
||||
return m.s.Set(k, buf.Bytes())
|
||||
sess.accessed = time.Now()
|
||||
b.sessions[sid] = sess
|
||||
return sess.data, nil
|
||||
}
|
||||
|
||||
func (m *mockMemRawStore) Get(k any) (ret any) {
|
||||
v, ok := m.s.Get(k).([]byte)
|
||||
if !ok {
|
||||
return nil
|
||||
func (b *memoryBackend) save(sid string, data []byte, create bool) error {
|
||||
b.lock.Lock()
|
||||
defer b.lock.Unlock()
|
||||
if _, exists := b.sessions[sid]; exists || create {
|
||||
b.sessions[sid] = memorySession{data: data, accessed: time.Now()}
|
||||
}
|
||||
var w map[string]any
|
||||
_ = gob.NewDecoder(bytes.NewBuffer(v)).Decode(&w)
|
||||
return w["v"]
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockMemRawStore) Delete(k any) error {
|
||||
return m.s.Delete(k)
|
||||
func (b *memoryBackend) destroy(sid string) error {
|
||||
b.lock.Lock()
|
||||
defer b.lock.Unlock()
|
||||
delete(b.sessions, sid)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockMemRawStore) ID() string {
|
||||
return m.s.ID()
|
||||
}
|
||||
|
||||
func (m *mockMemRawStore) Release() error {
|
||||
return m.s.Release()
|
||||
}
|
||||
|
||||
func (m *mockMemRawStore) Flush() error {
|
||||
return m.s.Flush()
|
||||
func (b *memoryBackend) gc() {
|
||||
b.lock.Lock()
|
||||
defer b.lock.Unlock()
|
||||
maps.DeleteFunc(b.sessions, func(_ string, sess memorySession) bool { return b.expired(sess) })
|
||||
}
|
||||
|
||||
type mockMemStore struct {
|
||||
*mockMemRawStore
|
||||
sid string
|
||||
data map[any][]byte
|
||||
}
|
||||
|
||||
var _ Store = (*mockMemStore)(nil)
|
||||
|
||||
func (m mockMemStore) Destroy(writer http.ResponseWriter, request *http.Request) error {
|
||||
// NewMockMemStore returns a store encoding each value like the real backends do, to catch values that can't be stored
|
||||
func NewMockMemStore(sid string) Store {
|
||||
return &mockMemStore{sid: sid, data: map[any][]byte{}}
|
||||
}
|
||||
|
||||
func (m *mockMemStore) Set(key, value any) error {
|
||||
encoded, err := util.PackData(map[any]any{key: value})
|
||||
if err == nil {
|
||||
m.data[key] = encoded
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *mockMemStore) Get(key any) any {
|
||||
var decoded map[any]any
|
||||
_ = util.UnpackData(m.data[key], &decoded)
|
||||
return decoded[key]
|
||||
}
|
||||
|
||||
func (m *mockMemStore) Delete(key any) error {
|
||||
delete(m.data, key)
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewMockMemStore(sid string) Store {
|
||||
return &mockMemStore{&mockMemRawStore{session.NewMemStore(sid)}}
|
||||
func (m *mockMemStore) ID() string {
|
||||
return m.sid
|
||||
}
|
||||
|
||||
func (m *mockMemStore) Release() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockMemStore) Flush() error {
|
||||
clear(m.data)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockMemStore) Destroy(http.ResponseWriter, *http.Request) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockMemStore) Regenerate(http.ResponseWriter, *http.Request) {}
|
||||
|
||||
+31
-187
@@ -6,214 +6,58 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"gitea.dev/modules/graceful"
|
||||
"gitea.dev/modules/nosql"
|
||||
|
||||
"gitea.com/go-chi/session"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
// RedisStore represents a redis session store implementation.
|
||||
type RedisStore struct {
|
||||
c redis.UniversalClient
|
||||
prefix, sid string
|
||||
duration time.Duration
|
||||
lock sync.RWMutex
|
||||
data map[any]any
|
||||
type redisBackend struct {
|
||||
client redis.UniversalClient
|
||||
prefix string
|
||||
maxLifetime time.Duration
|
||||
}
|
||||
|
||||
// NewRedisStore creates and returns a redis session store.
|
||||
func NewRedisStore(c redis.UniversalClient, prefix, sid string, dur time.Duration, kv map[any]any) *RedisStore {
|
||||
return &RedisStore{
|
||||
c: c,
|
||||
prefix: prefix,
|
||||
sid: sid,
|
||||
duration: dur,
|
||||
data: kv,
|
||||
func newRedisBackend(config string, maxLifetime int64) (*redisBackend, error) {
|
||||
uri := nosql.ToRedisURI(config)
|
||||
b := &redisBackend{
|
||||
client: nosql.GetManager().GetRedisClient(uri.String()),
|
||||
prefix: uri.Query().Get("prefix"),
|
||||
maxLifetime: time.Duration(maxLifetime) * time.Second,
|
||||
}
|
||||
return b, b.client.Ping(graceful.GetManager().ShutdownContext()).Err()
|
||||
}
|
||||
|
||||
// Set sets value to given key in session.
|
||||
func (s *RedisStore) Set(key, val any) error {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
s.data[key] = val
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get gets value by given key in session.
|
||||
func (s *RedisStore) Get(key any) any {
|
||||
s.lock.RLock()
|
||||
defer s.lock.RUnlock()
|
||||
|
||||
return s.data[key]
|
||||
}
|
||||
|
||||
// Delete delete a key from session.
|
||||
func (s *RedisStore) Delete(key any) error {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
delete(s.data, key)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ID returns current session ID.
|
||||
func (s *RedisStore) ID() string {
|
||||
return s.sid
|
||||
}
|
||||
|
||||
// Release releases resource and save data to provider.
|
||||
func (s *RedisStore) Release() error {
|
||||
// Skip encoding if the data is empty
|
||||
if len(s.data) == 0 {
|
||||
func (b *redisBackend) load(sid string) ([]byte, error) {
|
||||
ctx := graceful.GetManager().HammerContext()
|
||||
var get *redis.StringCmd
|
||||
_, err := b.client.Pipelined(ctx, func(pipe redis.Pipeliner) error {
|
||||
get = pipe.Get(ctx, b.prefix+sid)
|
||||
pipe.Expire(ctx, b.prefix+sid, b.maxLifetime)
|
||||
return nil
|
||||
})
|
||||
if errors.Is(err, redis.Nil) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
data, err := session.EncodeGob(s.data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.c.Set(graceful.GetManager().HammerContext(), s.prefix+s.sid, string(data), s.duration).Err()
|
||||
}
|
||||
|
||||
// Flush deletes all session data.
|
||||
func (s *RedisStore) Flush() error {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
s.data = make(map[any]any)
|
||||
return nil
|
||||
}
|
||||
|
||||
// RedisProvider represents a redis session provider implementation.
|
||||
type RedisProvider struct {
|
||||
c redis.UniversalClient
|
||||
duration time.Duration
|
||||
prefix string
|
||||
}
|
||||
|
||||
// Init initializes redis session provider.
|
||||
// configs: network=tcp,addr=:6379,password=macaron,db=0,pool_size=100,idle_timeout=180,prefix=session;
|
||||
func (p *RedisProvider) Init(maxlifetime int64, configs string) (err error) {
|
||||
p.duration, err = time.ParseDuration(fmt.Sprintf("%ds", maxlifetime))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
uri := nosql.ToRedisURI(configs)
|
||||
|
||||
for k, v := range uri.Query() {
|
||||
switch k {
|
||||
case "prefix":
|
||||
p.prefix = v[0]
|
||||
}
|
||||
}
|
||||
|
||||
p.c = nosql.GetManager().GetRedisClient(uri.String())
|
||||
return p.c.Ping(graceful.GetManager().ShutdownContext()).Err()
|
||||
}
|
||||
|
||||
// Read returns raw session store by session ID.
|
||||
func (p *RedisProvider) Read(sid string) (session.RawStore, error) {
|
||||
psid := p.prefix + sid
|
||||
if exist, err := p.Exist(sid); err == nil && !exist {
|
||||
if err := p.c.Set(graceful.GetManager().HammerContext(), psid, "", p.duration).Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var kv map[any]any
|
||||
kvs, err := p.c.Get(graceful.GetManager().HammerContext(), psid).Result()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(kvs) == 0 {
|
||||
kv = make(map[any]any)
|
||||
} else {
|
||||
kv, err = session.DecodeGob([]byte(kvs))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return NewRedisStore(p.c, p.prefix, sid, p.duration, kv), nil
|
||||
return get.Bytes()
|
||||
}
|
||||
|
||||
// Exist returns true if session with given ID exists.
|
||||
func (p *RedisProvider) Exist(sid string) (bool, error) {
|
||||
v, err := p.c.Exists(graceful.GetManager().HammerContext(), p.prefix+sid).Result()
|
||||
return err == nil && v == 1, err
|
||||
func (b *redisBackend) save(sid string, data []byte, create bool) error {
|
||||
ctx := graceful.GetManager().HammerContext()
|
||||
if create {
|
||||
return b.client.Set(ctx, b.prefix+sid, data, b.maxLifetime).Err()
|
||||
}
|
||||
return b.client.SetXX(ctx, b.prefix+sid, data, b.maxLifetime).Err()
|
||||
}
|
||||
|
||||
// Destroy deletes a session by session ID.
|
||||
func (p *RedisProvider) Destroy(sid string) error {
|
||||
return p.c.Del(graceful.GetManager().HammerContext(), p.prefix+sid).Err()
|
||||
func (b *redisBackend) destroy(sid string) error {
|
||||
return b.client.Del(graceful.GetManager().HammerContext(), b.prefix+sid).Err()
|
||||
}
|
||||
|
||||
// Regenerate regenerates a session store from old session ID to new one.
|
||||
func (p *RedisProvider) Regenerate(oldsid, sid string) (_ session.RawStore, err error) {
|
||||
poldsid := p.prefix + oldsid
|
||||
psid := p.prefix + sid
|
||||
|
||||
if exist, err := p.Exist(sid); err != nil {
|
||||
return nil, err
|
||||
} else if exist {
|
||||
return nil, fmt.Errorf("new sid '%s' already exists", sid)
|
||||
}
|
||||
if exist, err := p.Exist(oldsid); err == nil && !exist {
|
||||
// Make a fake old session.
|
||||
if err := p.c.Set(graceful.GetManager().HammerContext(), poldsid, "", p.duration).Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// do not use Rename here, because the old sid and new sid may be in different redis cluster slot.
|
||||
kvs, err := p.c.Get(graceful.GetManager().HammerContext(), poldsid).Result()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = p.c.Del(graceful.GetManager().HammerContext(), poldsid).Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = p.c.Set(graceful.GetManager().HammerContext(), psid, kvs, p.duration).Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var kv map[any]any
|
||||
if len(kvs) == 0 {
|
||||
kv = make(map[any]any)
|
||||
} else {
|
||||
kv, err = session.DecodeGob([]byte(kvs))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return NewRedisStore(p.c, p.prefix, sid, p.duration, kv), nil
|
||||
}
|
||||
|
||||
// Count counts and returns number of sessions.
|
||||
func (p *RedisProvider) Count() (int, error) {
|
||||
size, err := p.c.DBSize(graceful.GetManager().HammerContext()).Result()
|
||||
return int(size), err
|
||||
}
|
||||
|
||||
// GC calls GC to clean expired sessions.
|
||||
func (*RedisProvider) GC() {}
|
||||
|
||||
func init() {
|
||||
session.Register("redis", &RedisProvider{})
|
||||
}
|
||||
func (*redisBackend) gc() {}
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
// Copyright 2013 Beego Authors
|
||||
// Copyright 2014 The Macaron Authors
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package session
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"gitea.dev/modules/graceful"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/util"
|
||||
)
|
||||
|
||||
type backend interface {
|
||||
load(sid string) ([]byte, error) // returns nil for missing or expired sessions, refreshes the expiry of others
|
||||
save(sid string, data []byte, create bool) error // without create, only an existing session is updated
|
||||
destroy(sid string) error
|
||||
gc()
|
||||
}
|
||||
|
||||
// CHI-SESSION-GOB-REGISTER: packages must gob.Register the types they store at startup, so data stored before a restart still decodes
|
||||
func init() {
|
||||
gob.Register([]any{})
|
||||
gob.Register(map[int]any{})
|
||||
gob.Register(map[string]any{})
|
||||
gob.Register(map[any]any{})
|
||||
gob.Register(map[string]string{})
|
||||
gob.Register(map[int]string{})
|
||||
gob.Register(map[int]int{})
|
||||
gob.Register(map[int]int64{})
|
||||
}
|
||||
|
||||
func newBackend(provider, config string, maxLifetime int64) (backend, error) {
|
||||
switch provider {
|
||||
case "memory":
|
||||
return newMemoryBackend(maxLifetime), nil
|
||||
case "file":
|
||||
return newFileBackend(config, maxLifetime), nil
|
||||
case "redis":
|
||||
return newRedisBackend(config, maxLifetime)
|
||||
case "db":
|
||||
return &dbBackend{maxLifetime: maxLifetime}, nil
|
||||
}
|
||||
return nil, fmt.Errorf(`unsupported [session] PROVIDER %q, supported are "memory", "file", "redis" and "db", use "db" or "redis" to replace the removed "mysql", "postgres", "couchbase" and "memcache" providers`, provider)
|
||||
}
|
||||
|
||||
func Sessioner() (func(next http.Handler) http.Handler, error) {
|
||||
backend, err := newBackend(setting.SessionConfig.Provider, setting.SessionConfig.ProviderConfig, setting.SessionConfig.Maxlifetime)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
go runGC(graceful.GetManager().ShutdownContext(), backend, time.Duration(setting.SessionConfig.Gclifetime)*time.Second)
|
||||
return func(next http.Handler) http.Handler {
|
||||
return sessionHandler(backend, next)
|
||||
}, nil
|
||||
}
|
||||
|
||||
func sessionHandler(backend backend, next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
|
||||
sess, err := startSession(backend, resp, req)
|
||||
if err != nil {
|
||||
log.Error("Unable to start session: %v", err)
|
||||
resp.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(resp, req.WithContext(context.WithValue(req.Context(), ContextKey, sess)))
|
||||
if err := sess.Release(); err != nil {
|
||||
log.Error("Unable to release session: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func startSession(backend backend, resp http.ResponseWriter, req *http.Request) (*store, error) {
|
||||
sess := &store{backend: backend, resp: resp, data: map[any]any{}}
|
||||
cookie, err := req.Cookie(setting.SessionConfig.CookieName)
|
||||
if err != nil || !isValidSessionID(cookie.Value) {
|
||||
sess.sid = newSessionID()
|
||||
return sess, nil
|
||||
}
|
||||
sess.sid, sess.cookieSID = cookie.Value, cookie.Value
|
||||
encoded, err := backend.load(sess.sid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(encoded) == 0 {
|
||||
return sess, nil
|
||||
}
|
||||
var data map[any]any
|
||||
if err := util.UnpackData(encoded, &data); err != nil {
|
||||
log.Error("Unable to decode session data, starting with an empty session: %v", err)
|
||||
sess.stored, sess.changed = true, true
|
||||
} else if len(data) > 0 {
|
||||
sess.data, sess.stored = data, true
|
||||
}
|
||||
return sess, nil
|
||||
}
|
||||
|
||||
func newSessionID() string {
|
||||
// lower case (in case the file system is case-insensitive) and length=16 (db session's primary key is fixed size 16)
|
||||
// the entropy is about 36^16 > 80 bits
|
||||
return util.FastCryptoRandomString(16, "abcdefghijklmnopqrstuvwxyz0123456789")
|
||||
}
|
||||
|
||||
func isValidSessionID(sid string) bool {
|
||||
if len(sid) != 16 { // db session has a primary key with fixed size 16
|
||||
return false
|
||||
}
|
||||
for i := range len(sid) {
|
||||
c := sid[i]
|
||||
valid := (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z')
|
||||
if !valid {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func newCookie(value string) *http.Cookie {
|
||||
return &http.Cookie{
|
||||
Name: setting.SessionConfig.CookieName,
|
||||
Value: value,
|
||||
Path: util.IfZero(setting.SessionConfig.CookiePath, "/"),
|
||||
Domain: setting.SessionConfig.Domain,
|
||||
Secure: setting.SessionConfig.Secure,
|
||||
HttpOnly: true,
|
||||
SameSite: setting.SessionConfig.SameSite,
|
||||
}
|
||||
}
|
||||
|
||||
func runGC(ctx context.Context, backend backend, interval time.Duration) {
|
||||
for {
|
||||
backend.gc()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-time.After(interval):
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package session
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
auth_model "gitea.dev/models/auth"
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/test"
|
||||
"gitea.dev/modules/timeutil"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type handleFunc func(resp http.ResponseWriter, req *http.Request, sess Store)
|
||||
|
||||
type failingBackend struct {
|
||||
backend
|
||||
failDestroy bool
|
||||
}
|
||||
|
||||
func (b *failingBackend) destroy(sid string) error {
|
||||
if b.failDestroy {
|
||||
return errors.New("destroy failed")
|
||||
}
|
||||
return b.backend.destroy(sid)
|
||||
}
|
||||
|
||||
func TestSession(t *testing.T) {
|
||||
defer test.MockVariableValue(&setting.SessionConfig.CookiePath, "/sub")()
|
||||
defer test.MockVariableValue(&setting.SessionConfig.Secure, true)()
|
||||
|
||||
_, err := newBackend("mysql", "", 3600)
|
||||
assert.ErrorContains(t, err, `use "db" or "redis"`)
|
||||
|
||||
t.Run("GCStopsOnShutdown", func(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
cancel()
|
||||
runGC(ctx, newMemoryBackend(3600), time.Hour)
|
||||
})
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
newBackend func(t *testing.T) backend
|
||||
}{
|
||||
{
|
||||
name: "memory",
|
||||
newBackend: func(*testing.T) backend { return newMemoryBackend(3600) },
|
||||
},
|
||||
{
|
||||
name: "file",
|
||||
newBackend: func(t *testing.T) backend { return newFileBackend(t.TempDir(), 3600) },
|
||||
},
|
||||
{
|
||||
name: "db",
|
||||
newBackend: func(*testing.T) backend { return &dbBackend{maxLifetime: 3600} },
|
||||
},
|
||||
{
|
||||
name: "redis",
|
||||
newBackend: func(t *testing.T) backend {
|
||||
backend, err := newRedisBackend(test.PrepareTestRedis(t)+"?prefix=gitea-test-session-", 3600)
|
||||
require.NoError(t, err)
|
||||
return backend
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
backend := &failingBackend{backend: tc.newBackend(t)}
|
||||
serve := func(sid string, handle handleFunc) *httptest.ResponseRecorder {
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
if sid != "" {
|
||||
req.AddCookie(&http.Cookie{Name: setting.SessionConfig.CookieName, Value: sid})
|
||||
}
|
||||
resp := httptest.NewRecorder()
|
||||
sessionHandler(backend, http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
|
||||
handle(resp, req, GetContextSession(req))
|
||||
})).ServeHTTP(resp, req)
|
||||
return resp
|
||||
}
|
||||
create := func(t *testing.T) string {
|
||||
cookies := serve("", func(_ http.ResponseWriter, _ *http.Request, sess Store) {
|
||||
require.NoError(t, sess.Set("key", "value"))
|
||||
}).Result().Cookies()
|
||||
require.Len(t, cookies, 1)
|
||||
return cookies[0].Value
|
||||
}
|
||||
get := func(sid string, key any) (value any) {
|
||||
serve(sid, func(_ http.ResponseWriter, _ *http.Request, sess Store) { value = sess.Get(key) })
|
||||
return value
|
||||
}
|
||||
|
||||
t.Run("CookieOnlyOnceSessionHoldsData", func(t *testing.T) {
|
||||
assert.Empty(t, serve("", func(http.ResponseWriter, *http.Request, Store) {}).Result().Cookies())
|
||||
|
||||
resp := serve("../../etc/passwd", func(resp http.ResponseWriter, req *http.Request, sess Store) {
|
||||
require.NoError(t, sess.Set("key", "value"))
|
||||
require.NoError(t, sess.Set("other", 1))
|
||||
http.Redirect(resp, req, "https://example.com/", http.StatusSeeOther)
|
||||
})
|
||||
cookies := resp.Result().Cookies()
|
||||
require.Len(t, cookies, 1)
|
||||
sid := cookies[0].Value
|
||||
assert.True(t, isValidSessionID(sid))
|
||||
assert.Equal(t, "/sub", cookies[0].Path)
|
||||
assert.True(t, cookies[0].HttpOnly)
|
||||
assert.True(t, cookies[0].Secure)
|
||||
assert.Equal(t, http.SameSiteLaxMode, cookies[0].SameSite)
|
||||
switch sessionBackend := backend.backend.(type) {
|
||||
case *dbBackend:
|
||||
now := timeutil.TimeStampNow()
|
||||
_, err := db.GetEngine(t.Context()).ID(sid).Cols(auth_model.DbSessionLastAccessTime).Update(&auth_model.Session{LastAccessTime: now - 60})
|
||||
require.NoError(t, err)
|
||||
_, err = sessionBackend.load(sid)
|
||||
require.NoError(t, err)
|
||||
sess, _, err := auth_model.GetSession(t.Context(), sid)
|
||||
require.NoError(t, err)
|
||||
assert.GreaterOrEqual(t, sess.LastAccessTime, now)
|
||||
case *redisBackend:
|
||||
require.NoError(t, sessionBackend.client.Expire(t.Context(), "gitea-test-session-"+sid, time.Minute).Err())
|
||||
_, err := sessionBackend.load(sid)
|
||||
require.NoError(t, err)
|
||||
ttl, err := sessionBackend.client.TTL(t.Context(), "gitea-test-session-"+sid).Result()
|
||||
require.NoError(t, err)
|
||||
assert.Greater(t, ttl, time.Minute)
|
||||
}
|
||||
|
||||
resp = serve(sid, func(_ http.ResponseWriter, _ *http.Request, sess Store) {
|
||||
assert.Equal(t, sid, sess.ID())
|
||||
assert.Equal(t, "value", sess.Get("key"))
|
||||
require.NoError(t, sess.Set("key", "changed"))
|
||||
})
|
||||
assert.Empty(t, resp.Result().Cookies())
|
||||
})
|
||||
|
||||
t.Run("RegenerateMovesDataToNewID", func(t *testing.T) {
|
||||
oldSID := create(t)
|
||||
var newSID string
|
||||
resp := serve(oldSID, func(resp http.ResponseWriter, req *http.Request, sess Store) {
|
||||
sess.Regenerate(resp, req)
|
||||
newSID = sess.ID()
|
||||
})
|
||||
cookies := resp.Result().Cookies()
|
||||
require.Len(t, cookies, 1)
|
||||
assert.Equal(t, newSID, cookies[0].Value)
|
||||
assert.NotEqual(t, oldSID, newSID)
|
||||
assert.Nil(t, get(oldSID, "key"))
|
||||
assert.Equal(t, "value", get(newSID, "key"))
|
||||
|
||||
resp = serve("malformed", func(resp http.ResponseWriter, req *http.Request, sess Store) { sess.Regenerate(resp, req) })
|
||||
assert.Empty(t, resp.Result().Cookies())
|
||||
})
|
||||
|
||||
t.Run("DestroyIsNotUndoneByRelease", func(t *testing.T) {
|
||||
sid := create(t)
|
||||
var resp *httptest.ResponseRecorder
|
||||
serve(sid, func(_ http.ResponseWriter, _ *http.Request, concurrent Store) {
|
||||
resp = serve(sid, func(resp http.ResponseWriter, req *http.Request, sess Store) {
|
||||
require.NoError(t, sess.Flush())
|
||||
require.NoError(t, sess.Destroy(resp, req))
|
||||
})
|
||||
require.NoError(t, concurrent.Set("key", "changed"))
|
||||
})
|
||||
cookies := resp.Result().Cookies()
|
||||
require.Len(t, cookies, 1)
|
||||
assert.Equal(t, -1, cookies[0].MaxAge)
|
||||
assert.Equal(t, "/sub", cookies[0].Path)
|
||||
assert.True(t, cookies[0].Secure)
|
||||
encoded, err := backend.load(sid)
|
||||
require.NoError(t, err)
|
||||
assert.Nil(t, encoded)
|
||||
})
|
||||
|
||||
t.Run("FailedDestroyIsRetriedByRelease", func(t *testing.T) {
|
||||
sid := create(t)
|
||||
serve(sid, func(resp http.ResponseWriter, req *http.Request, sess Store) {
|
||||
backend.failDestroy = true
|
||||
require.Error(t, sess.Destroy(resp, req))
|
||||
backend.failDestroy = false
|
||||
assert.Nil(t, sess.Get("key"))
|
||||
})
|
||||
encoded, err := backend.load(sid)
|
||||
require.NoError(t, err)
|
||||
assert.Nil(t, encoded)
|
||||
})
|
||||
|
||||
t.Run("EmptiedSessionIsPersisted", func(t *testing.T) {
|
||||
for _, empty := range []func(Store) error{Store.Flush, func(sess Store) error { return sess.Delete("key") }} {
|
||||
sid := create(t)
|
||||
serve(sid, func(_ http.ResponseWriter, _ *http.Request, sess Store) { require.NoError(t, empty(sess)) })
|
||||
assert.Nil(t, get(sid, "key"))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("UnchangedReleaseKeepsConcurrentChanges", func(t *testing.T) {
|
||||
sid := create(t)
|
||||
serve(sid, func(_ http.ResponseWriter, _ *http.Request, reader Store) {
|
||||
serve(sid, func(_ http.ResponseWriter, _ *http.Request, writer Store) {
|
||||
require.NoError(t, writer.Set("key", "changed"))
|
||||
})
|
||||
require.NoError(t, reader.Delete("missing"))
|
||||
})
|
||||
assert.Equal(t, "changed", get(sid, "key"))
|
||||
})
|
||||
|
||||
t.Run("ConcurrentNewSessionWritesThrough", func(t *testing.T) {
|
||||
sid := newSessionID()
|
||||
serve(sid, func(_ http.ResponseWriter, _ *http.Request, first Store) {
|
||||
serve(sid, func(_ http.ResponseWriter, _ *http.Request, second Store) {
|
||||
require.NoError(t, second.Set("second", 2))
|
||||
})
|
||||
require.NoError(t, first.Set("first", 1))
|
||||
})
|
||||
assert.Equal(t, 1, get(sid, "first"))
|
||||
})
|
||||
|
||||
t.Run("UndecodableDataReadsAsEmptySession", func(t *testing.T) {
|
||||
sid := newSessionID()
|
||||
require.NoError(t, backend.save(sid, []byte("undecodable"), true))
|
||||
resp := serve(sid, func(_ http.ResponseWriter, _ *http.Request, sess Store) { assert.Nil(t, sess.Get("key")) })
|
||||
assert.Equal(t, http.StatusOK, resp.Code)
|
||||
encoded, err := backend.load(sid)
|
||||
require.NoError(t, err)
|
||||
assert.Nil(t, encoded)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileBackendWritesAtomicallyAndExpiresByModTime(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
backend := newFileBackend(root, 3600)
|
||||
sid := newSessionID()
|
||||
filename := filepath.Join(root, sid[0:1], sid[1:2], sid)
|
||||
require.NoError(t, backend.save(sid, []byte("first"), true))
|
||||
require.NoError(t, backend.save(sid, []byte("second"), false))
|
||||
|
||||
entries, err := os.ReadDir(filepath.Dir(filename))
|
||||
require.NoError(t, err)
|
||||
require.Len(t, entries, 1)
|
||||
assert.Equal(t, sid, entries[0].Name())
|
||||
encoded, err := backend.load(sid)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "second", string(encoded))
|
||||
|
||||
expired := time.Now().Add(-2 * time.Hour)
|
||||
require.NoError(t, os.Chtimes(filename, expired, expired))
|
||||
encoded, err = backend.load(sid)
|
||||
require.NoError(t, err)
|
||||
assert.Nil(t, encoded)
|
||||
backend.gc()
|
||||
assert.NoFileExists(t, filename)
|
||||
}
|
||||
+116
-20
@@ -5,43 +5,139 @@ package session
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"gitea.dev/modules/setting"
|
||||
|
||||
"gitea.com/go-chi/session"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/util"
|
||||
)
|
||||
|
||||
type RawStore = session.RawStore
|
||||
|
||||
type Store interface {
|
||||
RawStore
|
||||
Set(key, value any) error
|
||||
Get(key any) any
|
||||
Delete(key any) error
|
||||
ID() string
|
||||
Release() error
|
||||
Flush() error
|
||||
Destroy(http.ResponseWriter, *http.Request) error
|
||||
Regenerate(http.ResponseWriter, *http.Request)
|
||||
}
|
||||
|
||||
type mockStoreContextKeyStruct struct{}
|
||||
type store struct {
|
||||
backend backend
|
||||
resp http.ResponseWriter
|
||||
lock sync.RWMutex
|
||||
sid string
|
||||
cookieSID string // the session ID the client holds
|
||||
data map[any]any
|
||||
stored bool // the backend holds data for sid
|
||||
changed bool
|
||||
}
|
||||
|
||||
var MockStoreContextKey = mockStoreContextKeyStruct{}
|
||||
type contextKeyStruct struct{}
|
||||
|
||||
// RegenerateSession regenerates the underlying session and returns the new store
|
||||
func RegenerateSession(resp http.ResponseWriter, req *http.Request) (Store, error) {
|
||||
var ContextKey = contextKeyStruct{}
|
||||
|
||||
func (s *store) Set(key, value any) error {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
s.data[key] = value
|
||||
s.changed = true
|
||||
s.sendCookie(s.resp)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *store) Get(key any) any {
|
||||
s.lock.RLock()
|
||||
defer s.lock.RUnlock()
|
||||
return s.data[key]
|
||||
}
|
||||
|
||||
func (s *store) Delete(key any) error {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
if _, ok := s.data[key]; ok {
|
||||
delete(s.data, key)
|
||||
s.changed = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *store) ID() string {
|
||||
s.lock.RLock()
|
||||
defer s.lock.RUnlock()
|
||||
return s.sid
|
||||
}
|
||||
|
||||
func (s *store) Flush() error {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
clear(s.data)
|
||||
s.changed = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *store) Release() error {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
var err error
|
||||
switch {
|
||||
case !s.changed:
|
||||
return nil
|
||||
case len(s.data) > 0:
|
||||
var data []byte
|
||||
if data, err = util.PackData(s.data); err == nil {
|
||||
err = s.backend.save(s.sid, data, !s.stored)
|
||||
}
|
||||
case s.stored:
|
||||
err = s.backend.destroy(s.sid)
|
||||
}
|
||||
if err == nil {
|
||||
s.changed, s.stored = false, len(s.data) > 0
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *store) Destroy(resp http.ResponseWriter, _ *http.Request) error {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
err := s.backend.destroy(s.sid)
|
||||
cookie := newCookie("")
|
||||
cookie.MaxAge = -1
|
||||
http.SetCookie(resp, cookie)
|
||||
if err == nil {
|
||||
s.sid, s.stored = newSessionID(), false
|
||||
}
|
||||
s.cookieSID, s.changed = "", err != nil // Release retries a failed destroy
|
||||
clear(s.data)
|
||||
return err
|
||||
}
|
||||
|
||||
// Regenerate moves the session data to a new session ID, so an ID known before sign-in is never authenticated
|
||||
func (s *store) Regenerate(resp http.ResponseWriter, req *http.Request) {
|
||||
for _, f := range BeforeRegenerateSession {
|
||||
f(resp, req)
|
||||
}
|
||||
if setting.IsInTesting {
|
||||
if store, ok := req.Context().Value(MockStoreContextKey).(Store); ok {
|
||||
return store, nil
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
if s.stored {
|
||||
if err := s.backend.destroy(s.sid); err != nil {
|
||||
log.Error("Unable to destroy the regenerated session: %v", err)
|
||||
}
|
||||
}
|
||||
return session.RegenerateSession(resp, req)
|
||||
s.sid, s.stored, s.changed = newSessionID(), false, true
|
||||
s.sendCookie(resp)
|
||||
}
|
||||
|
||||
func (s *store) sendCookie(resp http.ResponseWriter) {
|
||||
if s.cookieSID != s.sid && len(s.data) > 0 {
|
||||
http.SetCookie(resp, newCookie(s.sid))
|
||||
s.cookieSID = s.sid
|
||||
}
|
||||
}
|
||||
|
||||
func GetContextSession(req *http.Request) Store {
|
||||
if setting.IsInTesting {
|
||||
if store, ok := req.Context().Value(MockStoreContextKey).(Store); ok {
|
||||
return store
|
||||
}
|
||||
}
|
||||
return session.GetSession(req)
|
||||
sess, _ := req.Context().Value(ContextKey).(Store)
|
||||
return sess
|
||||
}
|
||||
|
||||
// BeforeRegenerateSession is a list of functions that are called before a session is regenerated.
|
||||
|
||||
@@ -1,202 +0,0 @@
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package session
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"gitea.dev/modules/json"
|
||||
|
||||
"gitea.com/go-chi/session"
|
||||
couchbase "gitea.com/go-chi/session/couchbase"
|
||||
memcache "gitea.com/go-chi/session/memcache"
|
||||
mysql "gitea.com/go-chi/session/mysql"
|
||||
postgres "gitea.com/go-chi/session/postgres"
|
||||
)
|
||||
|
||||
// VirtualSessionProvider represents a shadowed session provider implementation.
|
||||
type VirtualSessionProvider struct {
|
||||
lock sync.RWMutex
|
||||
provider session.Provider
|
||||
}
|
||||
|
||||
// Init initializes the cookie session provider with the given config.
|
||||
func (o *VirtualSessionProvider) Init(gcLifetime int64, config string) error {
|
||||
var opts session.Options
|
||||
if err := json.Unmarshal([]byte(config), &opts); err != nil {
|
||||
return err
|
||||
}
|
||||
// Note that these options are unprepared so we can't just use NewManager here.
|
||||
// Nor can we access the provider map in session.
|
||||
// So we will just have to do this by hand.
|
||||
// This is only slightly more wrong than modules/setting/session.go:23
|
||||
switch opts.Provider {
|
||||
case "memory":
|
||||
o.provider = &session.MemProvider{}
|
||||
case "file":
|
||||
o.provider = &session.FileProvider{}
|
||||
case "redis":
|
||||
o.provider = &RedisProvider{}
|
||||
case "db":
|
||||
o.provider = &DBProvider{}
|
||||
case "mysql":
|
||||
o.provider = &mysql.MysqlProvider{}
|
||||
case "postgres":
|
||||
o.provider = &postgres.PostgresProvider{}
|
||||
case "couchbase":
|
||||
o.provider = &couchbase.CouchbaseProvider{}
|
||||
case "memcache":
|
||||
o.provider = &memcache.MemcacheProvider{}
|
||||
default:
|
||||
return fmt.Errorf("VirtualSessionProvider: Unknown Provider: %s", opts.Provider)
|
||||
}
|
||||
return o.provider.Init(gcLifetime, opts.ProviderConfig)
|
||||
}
|
||||
|
||||
// Read returns raw session store by session ID.
|
||||
func (o *VirtualSessionProvider) Read(sid string) (session.RawStore, error) {
|
||||
o.lock.RLock()
|
||||
defer o.lock.RUnlock()
|
||||
if exist, err := o.provider.Exist(sid); err == nil && exist {
|
||||
return o.provider.Read(sid)
|
||||
} else if err != nil {
|
||||
return nil, fmt.Errorf("check if '%s' exist failed: %w", sid, err)
|
||||
}
|
||||
kv := make(map[any]any)
|
||||
return NewVirtualStore(o, sid, kv), nil
|
||||
}
|
||||
|
||||
// Exist returns true if session with given ID exists.
|
||||
func (o *VirtualSessionProvider) Exist(sid string) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Destroy deletes a session by session ID.
|
||||
func (o *VirtualSessionProvider) Destroy(sid string) error {
|
||||
o.lock.Lock()
|
||||
defer o.lock.Unlock()
|
||||
return o.provider.Destroy(sid)
|
||||
}
|
||||
|
||||
// Regenerate regenerates a session store from old session ID to new one.
|
||||
func (o *VirtualSessionProvider) Regenerate(oldsid, sid string) (session.RawStore, error) {
|
||||
o.lock.Lock()
|
||||
defer o.lock.Unlock()
|
||||
return o.provider.Regenerate(oldsid, sid)
|
||||
}
|
||||
|
||||
// Count counts and returns number of sessions.
|
||||
func (o *VirtualSessionProvider) Count() (int, error) {
|
||||
o.lock.RLock()
|
||||
defer o.lock.RUnlock()
|
||||
return o.provider.Count()
|
||||
}
|
||||
|
||||
// GC calls GC to clean expired sessions.
|
||||
func (o *VirtualSessionProvider) GC() {
|
||||
o.provider.GC()
|
||||
}
|
||||
|
||||
func init() {
|
||||
session.Register("VirtualSession", &VirtualSessionProvider{})
|
||||
}
|
||||
|
||||
// VirtualStore represents a virtual session store implementation.
|
||||
type VirtualStore struct {
|
||||
p *VirtualSessionProvider
|
||||
sid string
|
||||
lock sync.RWMutex
|
||||
data map[any]any
|
||||
released bool
|
||||
}
|
||||
|
||||
// NewVirtualStore creates and returns a virtual session store.
|
||||
func NewVirtualStore(p *VirtualSessionProvider, sid string, kv map[any]any) *VirtualStore {
|
||||
return &VirtualStore{
|
||||
p: p,
|
||||
sid: sid,
|
||||
data: kv,
|
||||
}
|
||||
}
|
||||
|
||||
// Set sets value to given key in session.
|
||||
func (s *VirtualStore) Set(key, val any) error {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
s.data[key] = val
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get gets value by given key in session.
|
||||
func (s *VirtualStore) Get(key any) any {
|
||||
s.lock.RLock()
|
||||
defer s.lock.RUnlock()
|
||||
|
||||
return s.data[key]
|
||||
}
|
||||
|
||||
// Delete delete a key from session.
|
||||
func (s *VirtualStore) Delete(key any) error {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
delete(s.data, key)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ID returns current session ID.
|
||||
func (s *VirtualStore) ID() string {
|
||||
return s.sid
|
||||
}
|
||||
|
||||
// Release releases resource and save data to provider.
|
||||
func (s *VirtualStore) Release() error {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
// Now need to lock the provider
|
||||
s.p.lock.Lock()
|
||||
defer s.p.lock.Unlock()
|
||||
if len(s.data) > 0 {
|
||||
// Now ensure that we don't exist!
|
||||
realProvider := s.p.provider
|
||||
|
||||
if !s.released {
|
||||
if exist, err := realProvider.Exist(s.sid); err == nil && exist {
|
||||
// This is an error!
|
||||
return fmt.Errorf("new sid '%s' already exists", s.sid)
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("check if '%s' exist failed: %w", s.sid, err)
|
||||
}
|
||||
}
|
||||
realStore, err := realProvider.Read(s.sid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := realStore.Flush(); err != nil {
|
||||
return err
|
||||
}
|
||||
for key, value := range s.data {
|
||||
if err := realStore.Set(key, value); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
err = realStore.Release()
|
||||
if err == nil {
|
||||
s.released = true
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Flush deletes all session data.
|
||||
func (s *VirtualStore) Flush() error {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
s.data = make(map[any]any)
|
||||
return nil
|
||||
}
|
||||
@@ -151,7 +151,7 @@ func loadSecurityFrom(rootCfg ConfigProvider) {
|
||||
SecretKey = "!#@FDEWREWR&*("
|
||||
}
|
||||
|
||||
CookieRememberName = sec.Key("COOKIE_REMEMBER_NAME").MustString("gitea_incredible")
|
||||
CookieRememberName = sec.Key("COOKIE_REMEMBER_NAME").MustString("gitea_remember")
|
||||
|
||||
ReverseProxyAuthUser = sec.Key("REVERSE_PROXY_AUTHENTICATION_USER").MustString("X-WEBAUTH-USER")
|
||||
ReverseProxyAuthEmail = sec.Key("REVERSE_PROXY_AUTHENTICATION_EMAIL").MustString("X-WEBAUTH-EMAIL")
|
||||
|
||||
@@ -4,19 +4,17 @@
|
||||
package setting
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"gitea.dev/modules/json"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/util"
|
||||
)
|
||||
|
||||
// SessionConfig defines Session settings
|
||||
var SessionConfig = struct {
|
||||
OriginalProvider string
|
||||
Provider string
|
||||
Provider string
|
||||
// Provider configuration, it's corresponding to provider.
|
||||
ProviderConfig string
|
||||
// Cookie name to save session ID. Default is "MacaronSession".
|
||||
@@ -34,7 +32,8 @@ var SessionConfig = struct {
|
||||
// SameSite declares if your cookie should be restricted to a first-party or same-site context. Valid strings are "none", "lax", "strict". Default is "lax"
|
||||
SameSite http.SameSite
|
||||
}{
|
||||
CookieName: "i_like_gitea",
|
||||
Provider: "memory", // the "Install" page doesn't load the [session] config
|
||||
CookieName: "gitea_session",
|
||||
Gclifetime: 86400,
|
||||
Maxlifetime: 86400,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
@@ -42,7 +41,7 @@ var SessionConfig = struct {
|
||||
|
||||
func loadSessionFrom(rootCfg ConfigProvider) {
|
||||
sec := rootCfg.Section("session")
|
||||
SessionConfig.Provider = sec.Key("PROVIDER").In("file", []string{"memory", "file", "redis", "mysql", "postgres", "couchbase", "memcache", "db"})
|
||||
SessionConfig.Provider = sec.Key("PROVIDER").MustString("file")
|
||||
|
||||
switch SessionConfig.Provider {
|
||||
case "redis":
|
||||
@@ -59,12 +58,12 @@ func loadSessionFrom(rootCfg ConfigProvider) {
|
||||
SessionConfig.ProviderConfig = sec.Key("PROVIDER_CONFIG").String()
|
||||
}
|
||||
|
||||
SessionConfig.CookieName = sec.Key("COOKIE_NAME").MustString("i_like_gitea")
|
||||
// HINT: INSTALL-PAGE-COOKIE-INIT: the cookie system is not properly initialized on the Install page, so there is no CookiePath
|
||||
SessionConfig.CookieName = sec.Key("COOKIE_NAME").MustString("gitea_session")
|
||||
// HINT: INSTALL-PAGE-COOKIE-INIT: the cookie system is not properly initialized on the "Install" page, so there is no CookiePath
|
||||
SessionConfig.CookiePath = util.IfZero(AppSubURL, "/")
|
||||
SessionConfig.Secure = sec.Key("COOKIE_SECURE").MustBool(strings.HasPrefix(strings.ToLower(AppURL), "https://"))
|
||||
SessionConfig.Gclifetime = sec.Key("GC_INTERVAL_TIME").MustInt64(86400)
|
||||
SessionConfig.Maxlifetime = sec.Key("SESSION_LIFE_TIME").MustInt64(86400)
|
||||
SessionConfig.Gclifetime = cmp.Or(max(sec.Key("GC_INTERVAL_TIME").MustInt64(86400), 0), 3600)
|
||||
SessionConfig.Maxlifetime = cmp.Or(max(sec.Key("SESSION_LIFE_TIME").MustInt64(86400), 0), SessionConfig.Gclifetime)
|
||||
SessionConfig.Domain = sec.Key("DOMAIN").String()
|
||||
samesiteString := sec.Key("SAME_SITE").In("lax", []string{"none", "lax", "strict"})
|
||||
switch strings.ToLower(samesiteString) {
|
||||
@@ -75,11 +74,4 @@ func loadSessionFrom(rootCfg ConfigProvider) {
|
||||
default:
|
||||
SessionConfig.SameSite = http.SameSiteLaxMode
|
||||
}
|
||||
shadowConfig, err := json.Marshal(SessionConfig)
|
||||
if err != nil {
|
||||
log.Fatal("Can't shadow session config: %v", err)
|
||||
}
|
||||
SessionConfig.ProviderConfig = string(shadowConfig)
|
||||
SessionConfig.OriginalProvider = SessionConfig.Provider
|
||||
SessionConfig.Provider = "VirtualSession"
|
||||
}
|
||||
|
||||
@@ -69,7 +69,6 @@ PROVIDER = file
|
||||
|
||||
loadRedisFrom(cfg)
|
||||
loadSessionFrom(cfg)
|
||||
// ProviderConfig is shadowed into a JSON blob at the end of loadSessionFrom
|
||||
assert.Contains(t, SessionConfig.ProviderConfig, tt.wantContain)
|
||||
if tt.wantMissing != "" {
|
||||
assert.NotContains(t, SessionConfig.ProviderConfig, tt.wantMissing)
|
||||
@@ -77,3 +76,14 @@ PROVIDER = file
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSessionNonPositiveLifetimesUseDefaults(t *testing.T) {
|
||||
defer test.MockVariableValue(&SessionConfig)()
|
||||
for _, lifetime := range []string{"0", "-1"} {
|
||||
cfg, err := NewConfigProviderFromData("[session]\nGC_INTERVAL_TIME = " + lifetime + "\nSESSION_LIFE_TIME = " + lifetime)
|
||||
assert.NoError(t, err)
|
||||
loadSessionFrom(cfg)
|
||||
assert.EqualValues(t, 3600, SessionConfig.Gclifetime)
|
||||
assert.EqualValues(t, 3600, SessionConfig.Maxlifetime)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,6 +118,16 @@ func FastCryptoRandomBytes(length int) []byte {
|
||||
return buf
|
||||
}
|
||||
|
||||
func FastCryptoRandomString(length int, chars string) string {
|
||||
buf := make([]byte, length)
|
||||
limit := int64(len(chars))
|
||||
for i := range buf {
|
||||
num := FastCryptoRandomInt(limit)
|
||||
buf[i] = chars[num]
|
||||
}
|
||||
return string(buf)
|
||||
}
|
||||
|
||||
func FastCryptoRandomHex(length int) string {
|
||||
buf := FastCryptoRandomBytes(length / 2)
|
||||
return hex.EncodeToString(buf)
|
||||
|
||||
@@ -14,11 +14,11 @@ import (
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/public"
|
||||
"gitea.dev/modules/reqctx"
|
||||
"gitea.dev/modules/session"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/web/routing"
|
||||
"gitea.dev/services/context"
|
||||
|
||||
"gitea.com/go-chi/session"
|
||||
"github.com/chi-middleware/proxy"
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
@@ -122,24 +122,7 @@ func ForwardedHeadersHandler(limit int, trustedProxies []string) func(h http.Han
|
||||
}
|
||||
|
||||
func MustInitSessioner() func(next http.Handler) http.Handler {
|
||||
// TODO: CHI-SESSION-GOB-REGISTER: chi-session has a design problem: it calls gob.Register for "Set"
|
||||
// But if the server restarts, then the first "Get" will fail to decode the previously stored session data because the structs are not registered yet.
|
||||
// So each package should make sure their structs are registered correctly during startup for session storage.
|
||||
|
||||
middleware, err := session.Sessioner(session.Options{
|
||||
Provider: setting.SessionConfig.Provider,
|
||||
ProviderConfig: setting.SessionConfig.ProviderConfig,
|
||||
CookieName: setting.SessionConfig.CookieName,
|
||||
CookiePath: setting.SessionConfig.CookiePath,
|
||||
Gclifetime: setting.SessionConfig.Gclifetime,
|
||||
Maxlifetime: setting.SessionConfig.Maxlifetime,
|
||||
Secure: setting.SessionConfig.Secure,
|
||||
SameSite: setting.SessionConfig.SameSite,
|
||||
Domain: setting.SessionConfig.Domain,
|
||||
|
||||
// in the future, if websocket is used, the websocket handler should manage its own session sync (release)
|
||||
IgnoreReleaseForWebSocket: true,
|
||||
})
|
||||
middleware, err := session.Sessioner()
|
||||
if err != nil {
|
||||
log.Fatal("common.Sessioner failed: %v", err)
|
||||
}
|
||||
|
||||
@@ -18,8 +18,6 @@ import (
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/mailer"
|
||||
|
||||
"gitea.com/go-chi/session"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -93,20 +91,6 @@ func Config(ctx *context.Context) {
|
||||
ctx.Data["CacheItemTTL"] = setting.CacheService.TTL
|
||||
|
||||
sessionCfg := setting.SessionConfig
|
||||
if sessionCfg.Provider == "VirtualSession" {
|
||||
var realSession session.Options
|
||||
if err := json.Unmarshal([]byte(sessionCfg.ProviderConfig), &realSession); err != nil {
|
||||
log.Error("Unable to unmarshall session config for virtual provider config: %s\nError: %v", sessionCfg.ProviderConfig, err)
|
||||
}
|
||||
sessionCfg.Provider = realSession.Provider
|
||||
sessionCfg.ProviderConfig = realSession.ProviderConfig
|
||||
sessionCfg.CookieName = realSession.CookieName
|
||||
sessionCfg.CookiePath = realSession.CookiePath
|
||||
sessionCfg.Gclifetime = realSession.Gclifetime
|
||||
sessionCfg.Maxlifetime = realSession.Maxlifetime
|
||||
sessionCfg.Secure = realSession.Secure
|
||||
sessionCfg.Domain = realSession.Domain
|
||||
}
|
||||
sessionCfg.ProviderConfig = ""
|
||||
ctx.Data["SessionConfig"] = sessionCfg
|
||||
|
||||
|
||||
@@ -444,7 +444,9 @@ func extractUserNameFromOAuth2(gothUser *goth.User) (string, error) {
|
||||
// HandleSignOut resets the session and sets the cookies
|
||||
func HandleSignOut(ctx *context.Context) {
|
||||
_ = ctx.Session.Flush()
|
||||
_ = ctx.Session.Destroy(ctx.Resp, ctx.Req)
|
||||
if err := ctx.Session.Destroy(ctx.Resp, ctx.Req); err != nil {
|
||||
log.Error("Unable to destroy session: %v", err)
|
||||
}
|
||||
ctx.DeleteSiteCookie(setting.CookieRememberName)
|
||||
middleware.DeleteRedirectToCookie(ctx.Resp)
|
||||
}
|
||||
@@ -931,10 +933,8 @@ func ActivateEmail(ctx *context.Context) {
|
||||
}
|
||||
|
||||
func regenerateSession(ctx *context.Context, updates map[string]any) error {
|
||||
if _, err := session.RegenerateSession(ctx.Resp, ctx.Req); err != nil {
|
||||
return fmt.Errorf("regenerate session: %w", err)
|
||||
}
|
||||
sess := ctx.Session
|
||||
sess.Regenerate(ctx.Resp, ctx.Req)
|
||||
sessID := sess.ID()
|
||||
for k, v := range updates {
|
||||
if err := sess.Set(k, v); err != nil {
|
||||
|
||||
@@ -49,8 +49,6 @@ import (
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/forms"
|
||||
|
||||
_ "gitea.dev/modules/session" // to register all internal adapters
|
||||
|
||||
chi_middleware "github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/go-chi/cors"
|
||||
"github.com/klauspost/compress/gzhttp"
|
||||
|
||||
+5
-10
@@ -40,19 +40,14 @@ func Init() {
|
||||
// handleSignInNonInteractive clears existing session variables and stores new ones for the specified user object
|
||||
// it is mainly for middleware sign-in which doesn't need user's interaction.
|
||||
func handleSignInNonInteractive(resp http.ResponseWriter, req *http.Request, sess SessionStore, user *user_model.User) {
|
||||
// We need to regenerate the session...
|
||||
newSess, err := session.RegenerateSession(resp, req)
|
||||
if err != nil {
|
||||
log.Error(fmt.Sprintf("Error regenerating session: %v", err))
|
||||
} else {
|
||||
sess = newSess
|
||||
}
|
||||
|
||||
sess.Regenerate(resp, req)
|
||||
ClearSessionKeysForSignIn(sess)
|
||||
err = sess.Set(session.KeyUID, user.ID)
|
||||
if err != nil {
|
||||
if err := sess.Set(session.KeyUID, user.ID); err != nil {
|
||||
log.Error(fmt.Sprintf("Error setting session: %v", err))
|
||||
}
|
||||
if err := sess.Release(); err != nil { // save before a long-lived handler like a websocket runs
|
||||
log.Error("Error saving session: %v", err)
|
||||
}
|
||||
|
||||
opts := &user_service.UpdateOptions{SetLastLogin: true}
|
||||
// Language setting of the user overwrites the one previously set
|
||||
|
||||
@@ -34,6 +34,16 @@ func TestReverseProxyIgnoresBot(t *testing.T) {
|
||||
assert.Nil(t, user)
|
||||
}
|
||||
|
||||
type releaseCountingStore struct {
|
||||
session.Store
|
||||
released int
|
||||
}
|
||||
|
||||
func (s *releaseCountingStore) Release() error {
|
||||
s.released++
|
||||
return s.Store.Release()
|
||||
}
|
||||
|
||||
func TestReverseProxyLastLogin(t *testing.T) {
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
defer test.MockVariableValue(&setting.ReverseProxyAuthUser, "X-WEBAUTH-USER")()
|
||||
@@ -41,13 +51,15 @@ func TestReverseProxyLastLogin(t *testing.T) {
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
require.Zero(t, user.LastLoginUnix)
|
||||
|
||||
ctx, resp := contexttest.MockContext(t, "/", contexttest.MockContextOption{SessionStore: session.NewMockMemStore("reverse-proxy-last-login")})
|
||||
sess := &releaseCountingStore{Store: session.NewMockMemStore("reverse-proxy-last-login")}
|
||||
ctx, resp := contexttest.MockContext(t, "/", contexttest.MockContextOption{SessionStore: sess})
|
||||
ctx.Req.Header.Set(setting.ReverseProxyAuthUser, user.Name)
|
||||
rp := &ReverseProxy{CreateSession: true}
|
||||
|
||||
_, err := rp.Verify(ctx.Req, resp, ctx, ctx.Session)
|
||||
require.NoError(t, err)
|
||||
assert.NotZero(t, unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: user.ID}).LastLoginUnix)
|
||||
assert.Equal(t, 1, sess.released)
|
||||
|
||||
user.LastLoginUnix = 1
|
||||
require.NoError(t, user_model.UpdateUserCols(t.Context(), user, "last_login_unix"))
|
||||
@@ -55,4 +67,5 @@ func TestReverseProxyLastLogin(t *testing.T) {
|
||||
_, err = rp.Verify(ctx.Req, resp, ctx, ctx.Session)
|
||||
require.NoError(t, err)
|
||||
assert.EqualValues(t, 1, unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: user.ID}).LastLoginUnix) // no write without a new session
|
||||
assert.Equal(t, 1, sess.released)
|
||||
}
|
||||
|
||||
@@ -32,7 +32,6 @@ func (st *SessionsStore) New(r *http.Request, name string) (*sessions.Session, e
|
||||
return st.getOrNew(r, name, true)
|
||||
}
|
||||
|
||||
// getOrNew gets the session from the chi-session if it exists. Override permits the overriding of an unexpected object.
|
||||
func (st *SessionsStore) getOrNew(r *http.Request, name string, override bool) (*sessions.Session, error) {
|
||||
store := session_module.GetContextSession(r)
|
||||
|
||||
@@ -55,7 +54,7 @@ func (st *SessionsStore) getOrNew(r *http.Request, name string, override bool) (
|
||||
}
|
||||
|
||||
session.IsNew = override
|
||||
session.ID = store.ID() // Simply copy the session id from the chi store
|
||||
session.ID = store.ID()
|
||||
|
||||
return session, store.Set(name, session)
|
||||
}
|
||||
@@ -65,7 +64,7 @@ func (st *SessionsStore) Save(r *http.Request, w http.ResponseWriter, session *s
|
||||
store := session_module.GetContextSession(r)
|
||||
|
||||
if session.IsNew {
|
||||
_, _ = session_module.RegenerateSession(w, r)
|
||||
store.Regenerate(w, r)
|
||||
session.IsNew = false
|
||||
}
|
||||
|
||||
|
||||
@@ -149,16 +149,6 @@ func (b *Base) PlainText(status int, text string) {
|
||||
// Redirect redirects the request
|
||||
func (b *Base) Redirect(location string, status ...int) {
|
||||
code := util.OptionalArg(status, http.StatusSeeOther)
|
||||
|
||||
if !httplib.IsRelativeURL(location) {
|
||||
// Some browsers (Safari) have buggy behavior for Cookie + Cache + External Redirection, eg: /my-path => https://other/path
|
||||
// 1. the first request to "/my-path" contains cookie
|
||||
// 2. some time later, the request to "/my-path" doesn't contain cookie (caused by Prevent web tracking)
|
||||
// 3. Gitea's Sessioner doesn't see the session cookie, so it generates a new session id, and returns it to browser
|
||||
// 4. then the browser accepts the empty session, then the user is logged out
|
||||
// So in this case, we should remove the session cookie from the response header
|
||||
removeSessionCookieHeader(b.Resp)
|
||||
}
|
||||
// In case the request is made by "fetch-action" module, make JS redirect to the new location
|
||||
// Otherwise, the JS fetch will follow the redirection and read a "login" page, embed it to the current page, which is not expected.
|
||||
if httplib.IsGiteaFetchActionRequest(b.Req) {
|
||||
|
||||
@@ -21,27 +21,6 @@ func TestMain(m *testing.M) {
|
||||
|
||||
func TestRedirect(t *testing.T) {
|
||||
req, _ := http.NewRequest(http.MethodGet, "/", nil)
|
||||
|
||||
cases := []struct {
|
||||
url string
|
||||
keep bool
|
||||
}{
|
||||
{"http://test", false},
|
||||
{"https://test", false},
|
||||
{"//test", false},
|
||||
{"/://test", true},
|
||||
{"/test", true},
|
||||
}
|
||||
for _, c := range cases {
|
||||
resp := httptest.NewRecorder()
|
||||
b := NewBaseContextForTest(t, resp, req)
|
||||
resp.Header().Add("Set-Cookie", (&http.Cookie{Name: setting.SessionConfig.CookieName, Value: "dummy"}).String())
|
||||
b.Redirect(c.url)
|
||||
has := resp.Header().Get("Set-Cookie") == "i_like_gitea=dummy"
|
||||
assert.Equal(t, c.keep, has, "url = %q", c.url)
|
||||
}
|
||||
|
||||
req, _ = http.NewRequest(http.MethodGet, "/", nil)
|
||||
resp := httptest.NewRecorder()
|
||||
req.Header.Add("X-Gitea-Fetch-Action", "1")
|
||||
b := NewBaseContextForTest(t, resp, req)
|
||||
|
||||
@@ -4,26 +4,11 @@
|
||||
package context
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/web/middleware"
|
||||
)
|
||||
|
||||
const CookieNameFlash = "gitea_flash"
|
||||
|
||||
func removeSessionCookieHeader(w http.ResponseWriter) {
|
||||
cookies := w.Header()["Set-Cookie"]
|
||||
w.Header().Del("Set-Cookie")
|
||||
for _, cookie := range cookies {
|
||||
if strings.HasPrefix(cookie, setting.SessionConfig.CookieName+"=") {
|
||||
continue
|
||||
}
|
||||
w.Header().Add("Set-Cookie", cookie)
|
||||
}
|
||||
}
|
||||
|
||||
// SetSiteCookie convenience function to set most cookies consistently
|
||||
func (ctx *Context) SetSiteCookie(name, value string, maxAge int) {
|
||||
middleware.SetSiteCookie(ctx.Resp, name, value, maxAge)
|
||||
|
||||
@@ -18,16 +18,6 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestRemoveSessionCookieHeader(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
w.Header().Add("Set-Cookie", (&http.Cookie{Name: setting.SessionConfig.CookieName, Value: "foo"}).String())
|
||||
w.Header().Add("Set-Cookie", (&http.Cookie{Name: "other", Value: "bar"}).String())
|
||||
assert.Len(t, w.Header().Values("Set-Cookie"), 2)
|
||||
removeSessionCookieHeader(w)
|
||||
assert.Len(t, w.Header().Values("Set-Cookie"), 1)
|
||||
assert.Contains(t, "other=bar", w.Header().Get("Set-Cookie"))
|
||||
}
|
||||
|
||||
func TestServerErrorFetchActionRespondsJSON(t *testing.T) {
|
||||
req, _ := http.NewRequest(http.MethodPost, "/", nil)
|
||||
req.Header.Add("X-Gitea-Fetch-Action", "1")
|
||||
|
||||
@@ -70,7 +70,7 @@ func MockContext(t *testing.T, reqPath string, opts ...MockContextOption) (*cont
|
||||
ctx := context.NewWebContext(base, opt.Render, nil)
|
||||
ctx.SetContextValue(chi.RouteCtxKey, chiCtx)
|
||||
if opt.SessionStore != nil {
|
||||
ctx.SetContextValue(session.MockStoreContextKey, opt.SessionStore)
|
||||
ctx.SetContextValue(session.ContextKey, opt.SessionStore)
|
||||
ctx.Session = opt.SessionStore
|
||||
}
|
||||
ctx.Cache = cache.GetCache()
|
||||
|
||||
@@ -6,100 +6,52 @@ package integration
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"gitea.dev/modules/json"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/test"
|
||||
"gitea.dev/routers"
|
||||
"gitea.dev/tests"
|
||||
|
||||
"gitea.com/go-chi/session"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func getSessionID(t *testing.T, resp *httptest.ResponseRecorder) string {
|
||||
cookies := resp.Result().Cookies()
|
||||
found := false
|
||||
sessionID := ""
|
||||
for _, cookie := range cookies {
|
||||
func getSessionID(resp *httptest.ResponseRecorder) string {
|
||||
for _, cookie := range resp.Result().Cookies() {
|
||||
if cookie.Name == setting.SessionConfig.CookieName {
|
||||
sessionID = cookie.Value
|
||||
found = true
|
||||
return cookie.Value
|
||||
}
|
||||
}
|
||||
assert.True(t, found)
|
||||
assert.NotEmpty(t, sessionID)
|
||||
return sessionID
|
||||
}
|
||||
|
||||
func sessionFile(tmpDir, sessionID string) string {
|
||||
return filepath.Join(tmpDir, sessionID[0:1], sessionID[1:2], sessionID)
|
||||
}
|
||||
|
||||
func sessionFileExist(t *testing.T, tmpDir, sessionID string) bool {
|
||||
sessionFile := sessionFile(tmpDir, sessionID)
|
||||
_, err := os.Lstat(sessionFile)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return false
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
return true
|
||||
return ""
|
||||
}
|
||||
|
||||
func TestSessionFileCreation(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
defer test.MockVariableValue(&setting.SessionConfig.ProviderConfig)()
|
||||
defer test.MockVariableValue(&testWebRoutes)()
|
||||
|
||||
var config session.Options
|
||||
err := json.Unmarshal([]byte(setting.SessionConfig.ProviderConfig), &config)
|
||||
assert.NoError(t, err)
|
||||
|
||||
config.Provider = "file"
|
||||
|
||||
// Now create a temporaryDirectory
|
||||
tmpDir := t.TempDir()
|
||||
config.ProviderConfig = tmpDir
|
||||
|
||||
newConfigBytes, err := json.Marshal(config)
|
||||
assert.NoError(t, err)
|
||||
|
||||
setting.SessionConfig.ProviderConfig = string(newConfigBytes)
|
||||
|
||||
testWebRoutes = routers.NormalRoutes()
|
||||
defer test.MockVariableValue(&setting.SessionConfig.Provider, "file")()
|
||||
defer test.MockVariableValue(&setting.SessionConfig.ProviderConfig, tmpDir)()
|
||||
defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())()
|
||||
|
||||
t.Run("NoSessionOnViewIssue", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
req := NewRequest(t, "GET", "/user2/repo1/issues/1")
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
sessionID := getSessionID(t, resp)
|
||||
|
||||
// We're not logged in so there should be no session
|
||||
assert.False(t, sessionFileExist(t, tmpDir, sessionID))
|
||||
resp := MakeRequest(t, NewRequest(t, "GET", "/user2/repo1/issues/1"), http.StatusOK)
|
||||
assert.Empty(t, getSessionID(resp))
|
||||
})
|
||||
t.Run("CreateSessionOnLogin", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
req := NewRequest(t, "GET", "/user/login")
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
sessionID := getSessionID(t, resp)
|
||||
resp := MakeRequest(t, NewRequest(t, "GET", "/user/login"), http.StatusOK)
|
||||
assert.Empty(t, getSessionID(resp))
|
||||
|
||||
// We're not logged in so there should be no session
|
||||
assert.False(t, sessionFileExist(t, tmpDir, sessionID))
|
||||
|
||||
req = NewRequestWithValues(t, "POST", "/user/login", map[string]string{
|
||||
req := NewRequestWithValues(t, "POST", "/user/login", map[string]string{
|
||||
"user_name": "user2",
|
||||
"password": userPassword,
|
||||
})
|
||||
resp = MakeRequest(t, req, http.StatusSeeOther)
|
||||
sessionID = getSessionID(t, resp)
|
||||
|
||||
assert.FileExists(t, sessionFile(tmpDir, sessionID))
|
||||
sessionID := getSessionID(MakeRequest(t, req, http.StatusSeeOther))
|
||||
require.Len(t, sessionID, 16)
|
||||
assert.FileExists(t, filepath.Join(tmpDir, sessionID[0:1], sessionID[1:2], sessionID))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -68,8 +68,7 @@ func testViewRepoPublic(t *testing.T) {
|
||||
req = NewRequest(t, "GET", "/org3/repo3")
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
|
||||
session = loginUser(t, "user1")
|
||||
session.MakeRequest(t, req, http.StatusNotFound)
|
||||
loginUser(t, "user1").MakeRequest(t, NewRequest(t, "GET", "/org3/repo3"), http.StatusOK)
|
||||
}
|
||||
|
||||
func testViewRepoWithCache(t *testing.T) {
|
||||
|
||||
@@ -5,32 +5,31 @@ package integration
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea.dev/models/auth"
|
||||
"gitea.dev/models/unittest"
|
||||
"gitea.dev/modules/timeutil"
|
||||
"gitea.dev/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_RegenerateSession(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
func TestUpdateSession(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
defer timeutil.MockSet(time.Now())()
|
||||
key := "0123456789abcdef"
|
||||
for _, create := range []bool{true, true, false} {
|
||||
require.NoError(t, auth.UpdateSession(t.Context(), key, []byte("data"), create))
|
||||
}
|
||||
sess, exist, err := auth.GetSession(t.Context(), key)
|
||||
require.NoError(t, err)
|
||||
require.True(t, exist)
|
||||
assert.Equal(t, []byte("data"), sess.Data)
|
||||
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
key := "new_key890123456" // it must be 16 characters long
|
||||
key2 := "new_key890123457" // it must be 16 characters
|
||||
exist, err := auth.ExistSession(t.Context(), key)
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, auth.DestroySession(t.Context(), key))
|
||||
require.NoError(t, auth.UpdateSession(t.Context(), key, []byte("data"), false))
|
||||
_, exist, err = auth.GetSession(t.Context(), key)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, exist)
|
||||
|
||||
sess, err := auth.RegenerateSession(t.Context(), "", key)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, key, sess.Key)
|
||||
assert.Empty(t, sess.Data)
|
||||
|
||||
sess, err = auth.ReadSession(t.Context(), key2)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, key2, sess.Key)
|
||||
assert.Empty(t, sess.Data)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user