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:
silverwind
2026-10-02 21:08:14 +02:00
committed by GitHub
parent bea6fcaa84
commit cf89ecd887
34 changed files with 913 additions and 981 deletions
+16 -64
View File
@@ -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))
})
}
+1 -2
View File
@@ -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) {
+18 -19
View File
@@ -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)
}