forgejo/modules/session/db.go
Nils Goroll 31fff54e17
Improvement: Do not set session cookie for empty session
This is based on https://code.forgejo.org/go-chi/session/pulls/80.

The remainder of this message is largely copied from there:

For interoperability with reverse proxies and CDNs, setting a session
cookie for no good reason (login is a good reason) is a PITA, because it
makes caching of content for anonymous (not logged-in) users very hard,
requiring all kinds of special casing and error prone workarounds.

In particular in an age of exploitative AI bot crawling, being able to
serve content for anonymous users from a fast, efficient page cache is
an important option.

This patch lays a foundation by using an option added to go-chi/session
to not create session cookies always, but rather only when the
respective session is non-empty.

Test cases are included there and omitted here.
2026-03-11 04:18:06 +01:00

176 lines
3.8 KiB
Go

// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package session
import (
"log"
"sync"
"forgejo.org/models/auth"
"forgejo.org/models/db"
"forgejo.org/modules/timeutil"
"code.forgejo.org/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
}
// 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(db.DefaultContext, 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
}
// True if no keys have been set
func (s *DBStore) Empty() bool {
return len(s.data) == 0
}
// 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(db.DefaultContext, 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
}
// Exist returns true if session with given ID exists.
func (p *DBProvider) Exist(sid string) bool {
has, err := auth.ExistSession(db.DefaultContext, sid)
if err != nil {
panic("session/DB: error checking existence: " + err.Error())
}
return has
}
// Destroy deletes a session by session ID.
func (p *DBProvider) Destroy(sid string) error {
return auth.DestroySession(db.DefaultContext, 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(db.DefaultContext, 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 {
total, err := auth.CountSessions(db.DefaultContext)
if err != nil {
panic("session/DB: error counting records: " + err.Error())
}
return int(total)
}
// GC calls GC to clean expired sessions.
func (p *DBProvider) GC() {
if err := auth.CleanupSessions(db.DefaultContext, p.maxLifetime); err != nil {
log.Printf("session/DB: error garbage collecting: %v", err)
}
}
func init() {
session.Register("db", &DBProvider{})
}