rebuilt the session store and fixed the bugs we were seeing
This commit is contained in:
@@ -7,14 +7,12 @@ require (
|
|||||||
github.com/alexflint/go-arg v1.6.0
|
github.com/alexflint/go-arg v1.6.0
|
||||||
github.com/biter777/countries v1.7.5
|
github.com/biter777/countries v1.7.5
|
||||||
github.com/go-sql-driver/mysql v1.9.3
|
github.com/go-sql-driver/mysql v1.9.3
|
||||||
github.com/google/uuid v1.6.0
|
|
||||||
github.com/gorilla/sessions v1.4.0
|
github.com/gorilla/sessions v1.4.0
|
||||||
github.com/hashicorp/golang-lru v1.0.2
|
github.com/hashicorp/golang-lru v1.0.2
|
||||||
github.com/jmoiron/sqlx v1.4.0
|
github.com/jmoiron/sqlx v1.4.0
|
||||||
github.com/labstack/echo-contrib v0.17.4
|
github.com/labstack/echo-contrib v0.17.4
|
||||||
github.com/labstack/echo/v4 v4.13.4
|
github.com/labstack/echo/v4 v4.13.4
|
||||||
github.com/labstack/gommon v0.4.2
|
github.com/labstack/gommon v0.4.2
|
||||||
github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b
|
|
||||||
github.com/sirupsen/logrus v1.9.3
|
github.com/sirupsen/logrus v1.9.3
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -18,8 +18,6 @@ github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1
|
|||||||
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
||||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
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/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o=
|
github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o=
|
||||||
github.com/gorilla/context v1.1.2/go.mod h1:KDPwT9i/MeWHiLl90fuTgrt4/wPcv75vFAZLaOOcbxM=
|
github.com/gorilla/context v1.1.2/go.mod h1:KDPwT9i/MeWHiLl90fuTgrt4/wPcv75vFAZLaOOcbxM=
|
||||||
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
|
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
|
||||||
@@ -46,8 +44,6 @@ github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o
|
|||||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b h1:aUNXCGgukb4gtY99imuIeoh8Vr0GSwAlYxPAhqZrpFc=
|
|
||||||
github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b/go.mod h1:wTPjTepVu7uJBYgZ0SdWHQlIas582j6cn2jgk4DDdlg=
|
|
||||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
|||||||
@@ -23,6 +23,14 @@ type RenderedSideboxItem struct {
|
|||||||
Flags map[string]bool
|
Flags map[string]bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LinkX dereferences the Link pointer safely.
|
||||||
|
func (item *RenderedSideboxItem) LinkX() string {
|
||||||
|
if item.Link == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return *item.Link
|
||||||
|
}
|
||||||
|
|
||||||
// RenderedSidebox is the data for a single rendered sidebox.
|
// RenderedSidebox is the data for a single rendered sidebox.
|
||||||
type RenderedSidebox struct {
|
type RenderedSidebox struct {
|
||||||
TemplateName string
|
TemplateName string
|
||||||
|
|||||||
+1
-1
@@ -287,7 +287,7 @@ func NewAmContext(ctxt echo.Context) (AmContext, error) {
|
|||||||
scratchpad: nil,
|
scratchpad: nil,
|
||||||
}
|
}
|
||||||
ctxt.Set("amsterdam_context", &rc)
|
ctxt.Set("amsterdam_context", &rc)
|
||||||
sess, err := session.Get("amsterdam_session", ctxt)
|
sess, err := session.Get("AMSTERDAM_SESSION", ctxt)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
rc.session = sess
|
rc.session = sess
|
||||||
sess.Options = defoptions
|
sess.Options = defoptions
|
||||||
|
|||||||
+129
-63
@@ -11,7 +11,10 @@
|
|||||||
package ui
|
package ui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/rand"
|
||||||
"encoding/gob"
|
"encoding/gob"
|
||||||
|
"encoding/hex"
|
||||||
|
"net/http"
|
||||||
"slices"
|
"slices"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
@@ -19,54 +22,124 @@ import (
|
|||||||
|
|
||||||
"git.erbosoft.com/amy/amsterdam/config"
|
"git.erbosoft.com/amy/amsterdam/config"
|
||||||
"git.erbosoft.com/amy/amsterdam/database"
|
"git.erbosoft.com/amy/amsterdam/database"
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/gorilla/sessions"
|
"github.com/gorilla/sessions"
|
||||||
"github.com/quasoft/memstore"
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SessionStore is the Gorilla session store used by Amsterdam.
|
// AmsterdamStore is our implewmentation of the Gorilla session store that works close to HttpSession in Java.
|
||||||
var SessionStore sessions.Store
|
type AmsterdamStore struct {
|
||||||
|
mutex sync.RWMutex
|
||||||
|
sessions map[string]*sessions.Session
|
||||||
|
maxEntries int
|
||||||
|
expiry time.Duration
|
||||||
|
sweepRunning atomic.Bool
|
||||||
|
}
|
||||||
|
|
||||||
// sessionTable is the global map of all sessions.
|
func createAmsterdamStore(exp time.Duration) *AmsterdamStore {
|
||||||
var sessionTable map[string]*sessions.Session
|
rc := AmsterdamStore{
|
||||||
|
sessions: make(map[string]*sessions.Session),
|
||||||
|
maxEntries: 0,
|
||||||
|
expiry: exp,
|
||||||
|
}
|
||||||
|
rc.sweepRunning.Store(true)
|
||||||
|
return &rc
|
||||||
|
}
|
||||||
|
|
||||||
// sessionTableMax is the maximum number of entries in the session table.
|
/* Get (from Store interface) retrieves a new or existing session for the request.
|
||||||
var sessionTableMax int = 0
|
* Parameters:
|
||||||
|
* r - The HTTP request object.
|
||||||
|
* name - The name of the session.
|
||||||
|
* Returns:
|
||||||
|
* Session pointer (new or existing)
|
||||||
|
* Standard Go error status.
|
||||||
|
*/
|
||||||
|
func (st *AmsterdamStore) Get(r *http.Request, name string) (*sessions.Session, error) {
|
||||||
|
cookie, err := r.Cookie(name)
|
||||||
|
if err == nil {
|
||||||
|
st.mutex.RLock()
|
||||||
|
session, ok := st.sessions[cookie.Value]
|
||||||
|
if ok {
|
||||||
|
session.IsNew = false
|
||||||
|
}
|
||||||
|
st.mutex.RUnlock()
|
||||||
|
if ok {
|
||||||
|
return session, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return st.New(r, name)
|
||||||
|
}
|
||||||
|
|
||||||
// sessionTableMutex is the mutex for the session table.
|
/* New (from Store interface) creates and returns a new session object.
|
||||||
var sessionTableMutex sync.RWMutex
|
* Parameters:
|
||||||
|
* r - The HTTP request object.
|
||||||
|
* name - The name of the session.
|
||||||
|
* Returns:
|
||||||
|
* New session pointer
|
||||||
|
* Standard Go error status.
|
||||||
|
*/
|
||||||
|
func (st *AmsterdamStore) New(r *http.Request, name string) (*sessions.Session, error) {
|
||||||
|
session := sessions.NewSession(st, name)
|
||||||
|
session.IsNew = true
|
||||||
|
idBytes := make([]byte, 32)
|
||||||
|
if _, err := rand.Read(idBytes); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
session.ID = hex.EncodeToString(idBytes)
|
||||||
|
st.mutex.Lock()
|
||||||
|
st.sessions[session.ID] = session
|
||||||
|
if len(st.sessions) > st.maxEntries {
|
||||||
|
st.maxEntries = len(st.sessions)
|
||||||
|
}
|
||||||
|
st.mutex.Unlock()
|
||||||
|
return session, nil
|
||||||
|
}
|
||||||
|
|
||||||
// sessionExpiry is the amount of time before a session expires.
|
/* Save (from Store interface) saves off the sessin information to the response.
|
||||||
var sessionExpiry time.Duration
|
* Parameters:
|
||||||
|
* r - The HTTP request object.
|
||||||
|
* w - The response writer object.
|
||||||
|
* session - The session pointer to be saved.
|
||||||
|
* Returns:
|
||||||
|
* Standard Go error status.
|
||||||
|
*/
|
||||||
|
func (st *AmsterdamStore) Save(r *http.Request, w http.ResponseWriter, session *sessions.Session) error {
|
||||||
|
st.mutex.Lock()
|
||||||
|
defer st.mutex.Unlock()
|
||||||
|
cookie := sessions.NewCookie(session.Name(), session.ID, session.Options)
|
||||||
|
http.SetCookie(w, cookie)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// sweepRunning is the running flag for session sweeping.
|
/* sweep sweeps sessions to remove expired ones.
|
||||||
var sweepRunning atomic.Bool
|
* Parameters:
|
||||||
|
* tick - Channel that "pulses" periodically to run the task.
|
||||||
// sweepSessions sweeps through the sessions table and removes any expired sessions.
|
* done - Channel we write to when we're done.
|
||||||
func sweepSessions(tick <-chan time.Time, done chan bool) {
|
*/
|
||||||
|
func (st *AmsterdamStore) sweep(tick <-chan time.Time, done chan bool) {
|
||||||
for range tick {
|
for range tick {
|
||||||
if sweepRunning.Load() {
|
if st.sweepRunning.Load() {
|
||||||
// phase 1 - identify expired sessions
|
// phase 1 - identify expired sessions
|
||||||
sessionTableMutex.RLock()
|
st.mutex.RLock()
|
||||||
zap := make([]string, 0, len(sessionTable))
|
zap := make([]string, 0, len(st.sessions))
|
||||||
for k, v := range sessionTable {
|
for k, v := range st.sessions {
|
||||||
lastTime := v.Values["lasthit"].(time.Time)
|
lastTime, ok := v.Values["lasthit"]
|
||||||
if time.Since(lastTime) > sessionExpiry {
|
if ok && time.Since(lastTime.(time.Time)) > st.expiry {
|
||||||
zap = append(zap, k)
|
zap = append(zap, k)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sessionTableMutex.RUnlock()
|
st.mutex.RUnlock()
|
||||||
|
|
||||||
// phase 2 - get rid of the expired sessions
|
// phase 2 - get rid of the expired sessions
|
||||||
for _, k := range zap {
|
for _, k := range zap {
|
||||||
sessionTableMutex.Lock()
|
st.mutex.Lock()
|
||||||
s := sessionTable[k]
|
s, ok := st.sessions[k]
|
||||||
delete(sessionTable, k)
|
if ok {
|
||||||
sessionTableMutex.Unlock()
|
delete(st.sessions, k)
|
||||||
for q := range s.Values {
|
for q := range s.Values {
|
||||||
delete(s.Values, q)
|
delete(s.Values, q)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
st.mutex.Unlock()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
break
|
break
|
||||||
@@ -75,6 +148,26 @@ func sweepSessions(tick <-chan time.Time, done chan bool) {
|
|||||||
done <- true
|
done <- true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// sessioninfo returns information about the sessions in the store.
|
||||||
|
func (st *AmsterdamStore) sessionInfo() (int, []string, int) {
|
||||||
|
anons := 0
|
||||||
|
users := make([]string, 0, len(st.sessions))
|
||||||
|
st.mutex.RLock()
|
||||||
|
for _, s := range st.sessions {
|
||||||
|
if s.Values["user_anon"].(bool) {
|
||||||
|
anons++
|
||||||
|
} else {
|
||||||
|
users = append(users, s.Values["user_name"].(string))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
st.mutex.RUnlock()
|
||||||
|
slices.Sort(users)
|
||||||
|
return anons, users, st.maxEntries
|
||||||
|
}
|
||||||
|
|
||||||
|
// SessionStore is the Gorilla session store used by Amsterdam.
|
||||||
|
var SessionStore *AmsterdamStore
|
||||||
|
|
||||||
// init registers the time.Time value to be gobbed.
|
// init registers the time.Time value to be gobbed.
|
||||||
func init() {
|
func init() {
|
||||||
gob.Register(time.Time{})
|
gob.Register(time.Time{})
|
||||||
@@ -82,12 +175,6 @@ func init() {
|
|||||||
|
|
||||||
// SetupSessionManager sets up the session manager.
|
// SetupSessionManager sets up the session manager.
|
||||||
func SetupSessionManager() func() {
|
func SetupSessionManager() func() {
|
||||||
// create session store
|
|
||||||
SessionStore = memstore.NewMemStore([]byte(config.GlobalConfig.Rendering.CookieKey))
|
|
||||||
|
|
||||||
// create session table
|
|
||||||
sessionTable = make(map[string]*sessions.Session)
|
|
||||||
|
|
||||||
// get the time for the session to expire
|
// get the time for the session to expire
|
||||||
d, err := time.ParseDuration(config.GlobalConfig.Site.SessionExpire)
|
d, err := time.ParseDuration(config.GlobalConfig.Site.SessionExpire)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -96,7 +183,9 @@ func SetupSessionManager() func() {
|
|||||||
panic(err.Error())
|
panic(err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sessionExpiry = d
|
|
||||||
|
// create session store
|
||||||
|
SessionStore = createAmsterdamStore(d)
|
||||||
|
|
||||||
// get the clock value to run sweeps
|
// get the clock value to run sweeps
|
||||||
d, err = time.ParseDuration("1s")
|
d, err = time.ParseDuration("1s")
|
||||||
@@ -105,13 +194,12 @@ func SetupSessionManager() func() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// set up the sweep runner
|
// set up the sweep runner
|
||||||
sweepRunning.Store(true)
|
|
||||||
tkr := time.NewTicker(d)
|
tkr := time.NewTicker(d)
|
||||||
done := make(chan bool)
|
done := make(chan bool)
|
||||||
go sweepSessions(tkr.C, done)
|
go SessionStore.sweep(tkr.C, done)
|
||||||
return func() {
|
return func() {
|
||||||
// stop the sweep runner
|
// stop the sweep runner
|
||||||
sweepRunning.Store(false)
|
SessionStore.sweepRunning.Store(false)
|
||||||
<-done
|
<-done
|
||||||
tkr.Stop()
|
tkr.Stop()
|
||||||
}
|
}
|
||||||
@@ -145,25 +233,15 @@ func setSessionAnon(session *sessions.Session) {
|
|||||||
|
|
||||||
// AmSessionFirstTime initializes the session after it's first created.
|
// AmSessionFirstTime initializes the session after it's first created.
|
||||||
func AmSessionFirstTime(session *sessions.Session) {
|
func AmSessionFirstTime(session *sessions.Session) {
|
||||||
key := uuid.NewString()
|
|
||||||
session.Values["key"] = key
|
|
||||||
setSessionAnon(session)
|
setSessionAnon(session)
|
||||||
sessionTableMutex.Lock()
|
|
||||||
sessionTable[key] = session
|
|
||||||
if len(sessionTable) > sessionTableMax {
|
|
||||||
sessionTableMax = len(sessionTable)
|
|
||||||
}
|
|
||||||
session.Values["lasthit"] = time.Now()
|
session.Values["lasthit"] = time.Now()
|
||||||
sessionTableMutex.Unlock()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// AmResetSession clears the specified session.
|
// AmResetSession clears the specified session.
|
||||||
func AmResetSession(session *sessions.Session) {
|
func AmResetSession(session *sessions.Session) {
|
||||||
key := session.Values["key"]
|
|
||||||
for k := range session.Values {
|
for k := range session.Values {
|
||||||
delete(session.Values, k)
|
delete(session.Values, k)
|
||||||
}
|
}
|
||||||
session.Values["key"] = key
|
|
||||||
setSessionAnon(session)
|
setSessionAnon(session)
|
||||||
session.Values["lasthit"] = time.Now()
|
session.Values["lasthit"] = time.Now()
|
||||||
}
|
}
|
||||||
@@ -180,17 +258,5 @@ func AmHitSession(session *sessions.Session) {
|
|||||||
* Maximum number of users ever in session table.
|
* Maximum number of users ever in session table.
|
||||||
*/
|
*/
|
||||||
func AmSessions() (int, []string, int) {
|
func AmSessions() (int, []string, int) {
|
||||||
anons := 0
|
return SessionStore.sessionInfo()
|
||||||
users := make([]string, 0, len(sessionTable))
|
|
||||||
sessionTableMutex.RLock()
|
|
||||||
for _, s := range sessionTable {
|
|
||||||
if s.Values["user_anon"].(bool) {
|
|
||||||
anons++
|
|
||||||
} else {
|
|
||||||
users = append(users, s.Values["user_name"].(string))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sessionTableMutex.RUnlock()
|
|
||||||
slices.Sort(users)
|
|
||||||
return anons, users, sessionTableMax
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
{{ range sb.Items }}
|
{{ range sb.Items }}
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
{{ if ! .Flags["nobullet"] }}<span class="mr-2">🟣</span>{{ end }}
|
{{ if ! .Flags["nobullet"] }}<span class="mr-2">🟣</span>{{ end }}
|
||||||
{{ if .Link != nil }}
|
{{ if .LinkX() == "" }}
|
||||||
{{ if .Flags["bold"] }}
|
{{ if .Flags["bold"] }}
|
||||||
<span class="font-bold text-sm">{{ .Text }}</span>
|
<span class="font-bold text-sm">{{ .Text }}</span>
|
||||||
{{ else }}
|
{{ else }}
|
||||||
@@ -26,9 +26,9 @@
|
|||||||
{{ end }}
|
{{ end }}
|
||||||
{{ else }}
|
{{ else }}
|
||||||
{{ if .Flags["bold"] }}
|
{{ if .Flags["bold"] }}
|
||||||
<a href="{{ .Link }}" class="text-blue-700 hover:text-blue-900 font-bold text-sm">{{ .Text }}</a>
|
<a href="{{ .LinkX() }}" class="text-blue-700 hover:text-blue-900 font-bold text-sm">{{ .Text }}</a>
|
||||||
{{ else }}
|
{{ else }}
|
||||||
<a href="{{ .Link }}" class="text-blue-700 hover:text-blue-900 text-sm">{{ .Text }}</a>
|
<a href="{{ .LinkX() }}" class="text-blue-700 hover:text-blue-900 text-sm">{{ .Text }}</a>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user