mirror of
https://github.com/go-gitea/gitea.git
synced 2026-10-18 22:36:43 +09: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:
1 parent
bea6fcaa84
commit
cf89ecd887
34 files changed
+913
-981
No files matched your search
+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.
|
||||
|
||||
Reference in new issue
Block a user