mirror of
https://github.com/go-gitea/gitea.git
synced 2026-10-03 10:50:50 +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:
@@ -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):
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user