rewrote session management code entirely to try and eliminate map race conditions
This commit is contained in:
@@ -24,7 +24,6 @@ import (
|
|||||||
"git.erbosoft.com/amy/amsterdam/htmlcheck"
|
"git.erbosoft.com/amy/amsterdam/htmlcheck"
|
||||||
"git.erbosoft.com/amy/amsterdam/ui"
|
"git.erbosoft.com/amy/amsterdam/ui"
|
||||||
"git.erbosoft.com/amy/amsterdam/util"
|
"git.erbosoft.com/amy/amsterdam/util"
|
||||||
"github.com/labstack/echo-contrib/session"
|
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
"github.com/labstack/echo/v4/middleware"
|
"github.com/labstack/echo/v4/middleware"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
@@ -43,8 +42,8 @@ func setupEcho() *echo.Echo {
|
|||||||
} else {
|
} else {
|
||||||
log.Warn("WARNING: --debug-panic in effect - DO NOT use this in production!")
|
log.Warn("WARNING: --debug-panic in effect - DO NOT use this in production!")
|
||||||
}
|
}
|
||||||
e.Use(LogrusMiddleware, session.Middleware(ui.SessionStore))
|
e.Use(LogrusMiddleware, ui.SessionStoreInjector, ui.ContextCreator)
|
||||||
e.Use(ui.ContextCreator, ui.IPBanTest, ui.CookieLoginTest)
|
e.Use(ui.IPBanTest, ui.CookieLoginTest)
|
||||||
|
|
||||||
fn := ui.AmWrap(NotImplPage)
|
fn := ui.AmWrap(NotImplPage)
|
||||||
e.GET("/TODO/*", fn)
|
e.GET("/TODO/*", fn)
|
||||||
@@ -120,7 +119,7 @@ func main() {
|
|||||||
defer closer()
|
defer closer()
|
||||||
htmlcheck.SetupDicts()
|
htmlcheck.SetupDicts()
|
||||||
ui.SetupTemplates()
|
ui.SetupTemplates()
|
||||||
closer = ui.SetupSessionManager()
|
closer = ui.SetupAmSessionManager()
|
||||||
defer closer()
|
defer closer()
|
||||||
closer = ui.SetupAmContext()
|
closer = ui.SetupAmContext()
|
||||||
defer closer()
|
defer closer()
|
||||||
|
|||||||
+21
-23
@@ -22,8 +22,6 @@ import (
|
|||||||
"git.erbosoft.com/amy/amsterdam/database"
|
"git.erbosoft.com/amy/amsterdam/database"
|
||||||
"git.erbosoft.com/amy/amsterdam/util"
|
"git.erbosoft.com/amy/amsterdam/util"
|
||||||
"github.com/CloudyKit/jet/v6"
|
"github.com/CloudyKit/jet/v6"
|
||||||
"github.com/gorilla/sessions"
|
|
||||||
"github.com/labstack/echo-contrib/session"
|
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
@@ -88,7 +86,7 @@ type amContext struct {
|
|||||||
httprc int
|
httprc int
|
||||||
rendervars jet.VarMap
|
rendervars jet.VarMap
|
||||||
outputType string
|
outputType string
|
||||||
session *sessions.Session
|
session AmSession
|
||||||
globals *database.Globals
|
globals *database.Globals
|
||||||
globalFlags *util.OptionSet
|
globalFlags *util.OptionSet
|
||||||
user *database.User
|
user *database.User
|
||||||
@@ -118,7 +116,7 @@ func (c *amContext) ClearLoginCookie() {
|
|||||||
|
|
||||||
// ClearSession clears the current session.
|
// ClearSession clears the current session.
|
||||||
func (c *amContext) ClearSession() {
|
func (c *amContext) ClearSession() {
|
||||||
AmResetSession(c.echoContext.Request().Context(), c.session)
|
c.session.Reset(c.echoContext.Request().Context())
|
||||||
c.user = nil
|
c.user = nil
|
||||||
c.effectiveLevel = 0
|
c.effectiveLevel = 0
|
||||||
}
|
}
|
||||||
@@ -131,7 +129,7 @@ func (c *amContext) Ctx() context.Context {
|
|||||||
// CurrentCommunity returns the current community, if one's been set.
|
// CurrentCommunity returns the current community, if one's been set.
|
||||||
func (c *amContext) CurrentCommunity() *database.Community {
|
func (c *amContext) CurrentCommunity() *database.Community {
|
||||||
if c.community == nil {
|
if c.community == nil {
|
||||||
cv, ok := AmSessionGet(c.session, "lastCommunity")
|
cv, ok := c.session.Get("lastCommunity")
|
||||||
if ok && !c.CurrentUser().IsAnon {
|
if ok && !c.CurrentUser().IsAnon {
|
||||||
c.SetCommunityContext(fmt.Sprintf("%d", cv))
|
c.SetCommunityContext(fmt.Sprintf("%d", cv))
|
||||||
}
|
}
|
||||||
@@ -142,7 +140,7 @@ func (c *amContext) CurrentCommunity() *database.Community {
|
|||||||
// CurrentUser returns the current user from the session.
|
// CurrentUser returns the current user from the session.
|
||||||
func (c *amContext) CurrentUser() *database.User {
|
func (c *amContext) CurrentUser() *database.User {
|
||||||
if c.user == nil {
|
if c.user == nil {
|
||||||
id, ok := AmSessionUid(c.session)
|
id, ok := c.session.Uid()
|
||||||
var err error
|
var err error
|
||||||
var u *database.User
|
var u *database.User
|
||||||
if ok {
|
if ok {
|
||||||
@@ -161,8 +159,7 @@ func (c *amContext) CurrentUser() *database.User {
|
|||||||
|
|
||||||
// CurrentUserId returns the current user ID.
|
// CurrentUserId returns the current user ID.
|
||||||
func (c *amContext) CurrentUserId() int32 {
|
func (c *amContext) CurrentUserId() int32 {
|
||||||
rc, ok := AmSessionUid(c.session)
|
if rc, ok := c.session.Uid(); ok {
|
||||||
if ok {
|
|
||||||
return rc
|
return rc
|
||||||
}
|
}
|
||||||
u, err := database.AmGetAnonUser(c.echoContext.Request().Context())
|
u, err := database.AmGetAnonUser(c.echoContext.Request().Context())
|
||||||
@@ -252,7 +249,7 @@ func (c *amContext) IsMemberLocked() bool {
|
|||||||
|
|
||||||
// LeftMenu returns the current left menu selector.
|
// LeftMenu returns the current left menu selector.
|
||||||
func (c *amContext) LeftMenu() string {
|
func (c *amContext) LeftMenu() string {
|
||||||
rc, ok := AmSessionGet(c.session, "leftMenu")
|
rc, ok := c.session.Get("leftMenu")
|
||||||
if ok {
|
if ok {
|
||||||
return rc.(string)
|
return rc.(string)
|
||||||
} else {
|
} else {
|
||||||
@@ -307,7 +304,7 @@ func (c *amContext) RemoteIP() string {
|
|||||||
* u - New user to associate with the context.
|
* u - New user to associate with the context.
|
||||||
*/
|
*/
|
||||||
func (c *amContext) ReplaceUser(u *database.User) {
|
func (c *amContext) ReplaceUser(u *database.User) {
|
||||||
AmSetSessionUser(c.session, u)
|
c.session.SetUser(u)
|
||||||
c.user = u
|
c.user = u
|
||||||
c.effectiveLevel = u.BaseLevel
|
c.effectiveLevel = u.BaseLevel
|
||||||
}
|
}
|
||||||
@@ -340,7 +337,7 @@ func (c *amContext) SetCommunityContext(param string) error {
|
|||||||
c.effectiveLevel = level
|
c.effectiveLevel = level
|
||||||
}
|
}
|
||||||
if mbr {
|
if mbr {
|
||||||
AmSessionPut(c.session, "lastCommunity", comm.Id)
|
c.session.Set("lastCommunity", comm.Id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -348,7 +345,7 @@ func (c *amContext) SetCommunityContext(param string) error {
|
|||||||
|
|
||||||
// SetLeftMenu sets the current topmost left menu name value.
|
// SetLeftMenu sets the current topmost left menu name value.
|
||||||
func (c *amContext) SetLeftMenu(name string) {
|
func (c *amContext) SetLeftMenu(name string) {
|
||||||
AmSessionPut(c.session, "leftMenu", name)
|
c.session.Set("leftMenu", name)
|
||||||
}
|
}
|
||||||
|
|
||||||
/* SetLoginCookie adds the login cookie to the result output.
|
/* SetLoginCookie adds the login cookie to the result output.
|
||||||
@@ -386,18 +383,18 @@ func (c *amContext) SetScratch(name string, val any) {
|
|||||||
|
|
||||||
// GetSession returns a session variable.
|
// GetSession returns a session variable.
|
||||||
func (c *amContext) GetSession(name string) any {
|
func (c *amContext) GetSession(name string) any {
|
||||||
rc, _ := AmSessionGet(c.session, "x."+name)
|
rc, _ := c.session.Get("x." + name)
|
||||||
return rc
|
return rc
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetSession sets a session variable.
|
// SetSession sets a session variable.
|
||||||
func (c *amContext) SetSession(name string, value any) {
|
func (c *amContext) SetSession(name string, value any) {
|
||||||
AmSessionPut(c.session, "x."+name, value)
|
c.session.Set("x."+name, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsSession tests to see whether a session value is set.
|
// IsSession tests to see whether a session value is set.
|
||||||
func (c *amContext) IsSession(name string) bool {
|
func (c *amContext) IsSession(name string) bool {
|
||||||
_, ok := AmSessionGet(c.session, "x."+name)
|
_, ok := c.session.Get("x." + name)
|
||||||
return ok
|
return ok
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -427,7 +424,7 @@ func (c *amContext) VarMap() jet.VarMap {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// defoptions is the default options for the HTTP session.
|
// defoptions is the default options for the HTTP session.
|
||||||
var defoptions *sessions.Options = &sessions.Options{
|
var defoptions *AmSessionOptions = &AmSessionOptions{
|
||||||
Path: "/",
|
Path: "/",
|
||||||
MaxAge: 86400,
|
MaxAge: 86400,
|
||||||
HttpOnly: true,
|
HttpOnly: true,
|
||||||
@@ -468,17 +465,18 @@ func newContext(ctxt echo.Context) (*amContext, error) {
|
|||||||
|
|
||||||
rc.echoContext = ctxt
|
rc.echoContext = ctxt
|
||||||
ctxt.Set("__amsterdam_context", rc)
|
ctxt.Set("__amsterdam_context", rc)
|
||||||
sess, err := session.Get("AMSTERDAM_SESSION", ctxt)
|
store := ctxt.Get("AmSessionStore").(AmSessionStore)
|
||||||
|
sess, err := store.Get(ctxt.Request(), "AMSTERDAM_SESSION")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
rc.session = sess
|
rc.session = sess
|
||||||
sess.Options = defoptions
|
sess.SetOptions(defoptions)
|
||||||
if sess.IsNew {
|
if sess.IsNew() {
|
||||||
AmSessionFirstTime(ctxt.Request().Context(), sess)
|
sess.FirstTime(ctxt.Request().Context())
|
||||||
} else {
|
} else {
|
||||||
AmHitSession(sess)
|
sess.Hit()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
id, ok := AmSessionUid(sess)
|
id, ok := sess.Uid()
|
||||||
if ok {
|
if ok {
|
||||||
rc.user, err = database.AmGetUser(ctxt.Request().Context(), id)
|
rc.user, err = database.AmGetUser(ctxt.Request().Context(), id)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -492,7 +490,7 @@ func newContext(ctxt echo.Context) (*amContext, error) {
|
|||||||
rc.effectiveLevel = database.AmRole("NotInList").Level()
|
rc.effectiveLevel = database.AmRole("NotInList").Level()
|
||||||
}
|
}
|
||||||
if rc.user != nil && !rc.user.IsAnon {
|
if rc.user != nil && !rc.user.IsAnon {
|
||||||
cp, ok := AmSessionGet(sess, "lastCommunity")
|
cp, ok := sess.Get("lastCommunity")
|
||||||
if ok {
|
if ok {
|
||||||
rc.SetCommunityContext(fmt.Sprintf("%d", cp))
|
rc.SetCommunityContext(fmt.Sprintf("%d", cp))
|
||||||
}
|
}
|
||||||
|
|||||||
+139
@@ -10,6 +10,7 @@
|
|||||||
package ui
|
package ui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -17,8 +18,20 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"git.erbosoft.com/amy/amsterdam/config"
|
||||||
|
"git.erbosoft.com/amy/amsterdam/database"
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
This is mainly a rewrite of parts of Gorilla Sessions, but with a more-defined session interface so that we can mutex-protect
|
||||||
|
the session variables, as our use case also dictates that the sessions be part of a global map in the session store so they can
|
||||||
|
be timed out as well as used to show the logged-in users. This is similar to the session support provided in J2EE servlets.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// AmSessionOptions gives the options for the session.
|
||||||
type AmSessionOptions struct {
|
type AmSessionOptions struct {
|
||||||
Path string
|
Path string
|
||||||
Domain string
|
Domain string
|
||||||
@@ -29,6 +42,7 @@ type AmSessionOptions struct {
|
|||||||
SameSite http.SameSite
|
SameSite http.SameSite
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// newCookieFromOptions creates a new HTTP cookie given the options.
|
||||||
func newCookieFromOptions(name, value string, options *AmSessionOptions) *http.Cookie {
|
func newCookieFromOptions(name, value string, options *AmSessionOptions) *http.Cookie {
|
||||||
return &http.Cookie{
|
return &http.Cookie{
|
||||||
Name: name,
|
Name: name,
|
||||||
@@ -43,12 +57,14 @@ func newCookieFromOptions(name, value string, options *AmSessionOptions) *http.C
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AmSession is the public session interface.
|
||||||
type AmSession interface {
|
type AmSession interface {
|
||||||
ID() string
|
ID() string
|
||||||
Name() string
|
Name() string
|
||||||
Save(*http.Request, http.ResponseWriter) error
|
Save(*http.Request, http.ResponseWriter) error
|
||||||
Store() AmSessionStore
|
Store() AmSessionStore
|
||||||
Options() *AmSessionOptions
|
Options() *AmSessionOptions
|
||||||
|
SetOptions(*AmSessionOptions)
|
||||||
IsNew() bool
|
IsNew() bool
|
||||||
SetNew(bool)
|
SetNew(bool)
|
||||||
AddFlash(value any, vars ...string)
|
AddFlash(value any, vars ...string)
|
||||||
@@ -56,8 +72,14 @@ type AmSession interface {
|
|||||||
Get(any) (any, bool)
|
Get(any) (any, bool)
|
||||||
Set(any, any)
|
Set(any, any)
|
||||||
Erase()
|
Erase()
|
||||||
|
Uid() (int32, bool)
|
||||||
|
SetUser(*database.User)
|
||||||
|
FirstTime(context.Context)
|
||||||
|
Reset(context.Context)
|
||||||
|
Hit()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AmSessionStore is the public interface to the session store.
|
||||||
type AmSessionStore interface {
|
type AmSessionStore interface {
|
||||||
Get(*http.Request, string) (AmSession, error)
|
Get(*http.Request, string) (AmSession, error)
|
||||||
New(*http.Request, string) (AmSession, error)
|
New(*http.Request, string) (AmSession, error)
|
||||||
@@ -65,6 +87,7 @@ type AmSessionStore interface {
|
|||||||
SessionInfo() (int, []string, int)
|
SessionInfo() (int, []string, int)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// amSession is the implementation structure for AmSession.
|
||||||
type amSession struct {
|
type amSession struct {
|
||||||
mutex sync.RWMutex
|
mutex sync.RWMutex
|
||||||
id string
|
id string
|
||||||
@@ -75,38 +98,51 @@ type amSession struct {
|
|||||||
name string
|
name string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// defaultFlashKey is the default sesison variable key for "flashes."
|
||||||
const defaultFlashKey = "__flash"
|
const defaultFlashKey = "__flash"
|
||||||
|
|
||||||
|
// ID returns the ID of the session.
|
||||||
func (sess *amSession) ID() string {
|
func (sess *amSession) ID() string {
|
||||||
return sess.id
|
return sess.id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Name returns the name of the session, used for the cookie name.
|
||||||
func (sess *amSession) Name() string {
|
func (sess *amSession) Name() string {
|
||||||
return sess.name
|
return sess.name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Save is a helper function that calls the session store to save this session.
|
||||||
func (sess *amSession) Save(r *http.Request, w http.ResponseWriter) error {
|
func (sess *amSession) Save(r *http.Request, w http.ResponseWriter) error {
|
||||||
return sess.store.Save(r, w, sess)
|
return sess.store.Save(r, w, sess)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Store returns the pointer to the session store.
|
||||||
func (sess *amSession) Store() AmSessionStore {
|
func (sess *amSession) Store() AmSessionStore {
|
||||||
return sess.store
|
return sess.store
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Options returns the options for this session.
|
||||||
func (sess *amSession) Options() *AmSessionOptions {
|
func (sess *amSession) Options() *AmSessionOptions {
|
||||||
return sess.options
|
return sess.options
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (sess *amSession) SetOptions(opt *AmSessionOptions) {
|
||||||
|
sess.options = opt
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsNew returns the "new" flag of this session.
|
||||||
func (sess *amSession) IsNew() bool {
|
func (sess *amSession) IsNew() bool {
|
||||||
return sess.isNew
|
return sess.isNew
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetNew sets the "new" flag of this session.
|
||||||
func (sess *amSession) SetNew(v bool) {
|
func (sess *amSession) SetNew(v bool) {
|
||||||
sess.mutex.Lock()
|
sess.mutex.Lock()
|
||||||
sess.isNew = v
|
sess.isNew = v
|
||||||
sess.mutex.Unlock()
|
sess.mutex.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AddFlash adds a "flash message" to the session. The second parameter allows optionally specifying the variable name.
|
||||||
func (sess *amSession) AddFlash(value any, vars ...string) {
|
func (sess *amSession) AddFlash(value any, vars ...string) {
|
||||||
key := defaultFlashKey
|
key := defaultFlashKey
|
||||||
if len(vars) > 0 {
|
if len(vars) > 0 {
|
||||||
@@ -121,6 +157,7 @@ func (sess *amSession) AddFlash(value any, vars ...string) {
|
|||||||
sess.values[key] = append(flashes, value)
|
sess.values[key] = append(flashes, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Flashes retrueves all "flash messages" from the session. The second parameter allows optionally specifying the variable name.
|
||||||
func (sess *amSession) Flashes(vars ...string) []any {
|
func (sess *amSession) Flashes(vars ...string) []any {
|
||||||
var flashes []any
|
var flashes []any
|
||||||
key := defaultFlashKey
|
key := defaultFlashKey
|
||||||
@@ -136,6 +173,7 @@ func (sess *amSession) Flashes(vars ...string) []any {
|
|||||||
return flashes
|
return flashes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get gets a session variable.
|
||||||
func (sess *amSession) Get(key any) (any, bool) {
|
func (sess *amSession) Get(key any) (any, bool) {
|
||||||
sess.mutex.RLock()
|
sess.mutex.RLock()
|
||||||
defer sess.mutex.RUnlock()
|
defer sess.mutex.RUnlock()
|
||||||
@@ -143,12 +181,14 @@ func (sess *amSession) Get(key any) (any, bool) {
|
|||||||
return v, ok
|
return v, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set sets a session variable.
|
||||||
func (sess *amSession) Set(key, value any) {
|
func (sess *amSession) Set(key, value any) {
|
||||||
sess.mutex.Lock()
|
sess.mutex.Lock()
|
||||||
defer sess.mutex.Unlock()
|
defer sess.mutex.Unlock()
|
||||||
sess.values[key] = value
|
sess.values[key] = value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Erase erases all session variables.
|
||||||
func (sess *amSession) Erase() {
|
func (sess *amSession) Erase() {
|
||||||
sess.mutex.Lock()
|
sess.mutex.Lock()
|
||||||
defer sess.mutex.Unlock()
|
defer sess.mutex.Unlock()
|
||||||
@@ -157,6 +197,52 @@ func (sess *amSession) Erase() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Uid returns the current user ID associated with this session.
|
||||||
|
func (sess *amSession) Uid() (int32, bool) {
|
||||||
|
if rc, ok := sess.Get("user_id"); ok {
|
||||||
|
return rc.(int32), ok
|
||||||
|
}
|
||||||
|
return -1, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetUser sets a user into the session, saving off the username, ID, and anonymous flag.
|
||||||
|
func (sess *amSession) SetUser(user *database.User) {
|
||||||
|
sess.mutex.Lock()
|
||||||
|
defer sess.mutex.Unlock()
|
||||||
|
sess.values["user_id"] = user.Uid
|
||||||
|
sess.values["user_name"] = user.Username
|
||||||
|
sess.values["user_anon"] = user.IsAnon
|
||||||
|
}
|
||||||
|
|
||||||
|
// setAnon sets this session to contain the anonymous user.
|
||||||
|
func (sess *amSession) setAnon(ctx context.Context) {
|
||||||
|
u, err := database.AmGetAnonUser(ctx)
|
||||||
|
if err == nil {
|
||||||
|
sess.SetUser(u)
|
||||||
|
} else {
|
||||||
|
log.Errorf("unable to set anonymous user: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FirstTime prepares the session after it was just created.
|
||||||
|
func (sess *amSession) FirstTime(ctx context.Context) {
|
||||||
|
sess.setAnon(ctx)
|
||||||
|
sess.Set("lasthit", time.Now())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset resets a session after it's been timed out.
|
||||||
|
func (sess *amSession) Reset(ctx context.Context) {
|
||||||
|
sess.Erase()
|
||||||
|
sess.setAnon(ctx)
|
||||||
|
sess.Set("lasthit", time.Now())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hit updates the last-hit time in the session.
|
||||||
|
func (sess *amSession) Hit() {
|
||||||
|
sess.Set("lasthit", time.Now())
|
||||||
|
}
|
||||||
|
|
||||||
|
// amSessionStore is the implementatiuon structure for AmSessionStore.
|
||||||
type amSessionStore struct {
|
type amSessionStore struct {
|
||||||
mutex sync.RWMutex
|
mutex sync.RWMutex
|
||||||
sessions map[string]*amSession
|
sessions map[string]*amSession
|
||||||
@@ -165,6 +251,7 @@ type amSessionStore struct {
|
|||||||
sweepRunning atomic.Bool
|
sweepRunning atomic.Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// createAmSessionStore creates the session store.
|
||||||
func createAmSessionStore(exp time.Duration) *amSessionStore {
|
func createAmSessionStore(exp time.Duration) *amSessionStore {
|
||||||
rc := &amSessionStore{
|
rc := &amSessionStore{
|
||||||
sessions: make(map[string]*amSession),
|
sessions: make(map[string]*amSession),
|
||||||
@@ -175,6 +262,7 @@ func createAmSessionStore(exp time.Duration) *amSessionStore {
|
|||||||
return rc
|
return rc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get retrieves a session from the request cookie.
|
||||||
func (st *amSessionStore) Get(r *http.Request, name string) (AmSession, error) {
|
func (st *amSessionStore) Get(r *http.Request, name string) (AmSession, error) {
|
||||||
cookie, err := r.Cookie(name)
|
cookie, err := r.Cookie(name)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -191,6 +279,7 @@ func (st *amSessionStore) Get(r *http.Request, name string) (AmSession, error) {
|
|||||||
return st.New(r, name)
|
return st.New(r, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// New creates a new session.
|
||||||
func (st *amSessionStore) New(r *http.Request, name string) (AmSession, error) {
|
func (st *amSessionStore) New(r *http.Request, name string) (AmSession, error) {
|
||||||
session := &amSession{
|
session := &amSession{
|
||||||
values: make(map[any]any),
|
values: make(map[any]any),
|
||||||
@@ -213,6 +302,7 @@ func (st *amSessionStore) New(r *http.Request, name string) (AmSession, error) {
|
|||||||
return session, nil
|
return session, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Save saves the session identifier to the response cookies.
|
||||||
func (st *amSessionStore) Save(r *http.Request, w http.ResponseWriter, sess AmSession) error {
|
func (st *amSessionStore) Save(r *http.Request, w http.ResponseWriter, sess AmSession) error {
|
||||||
cookie := newCookieFromOptions(sess.Name(), sess.ID(), sess.Options())
|
cookie := newCookieFromOptions(sess.Name(), sess.ID(), sess.Options())
|
||||||
if sess.Options().MaxAge > 0 {
|
if sess.Options().MaxAge > 0 {
|
||||||
@@ -225,6 +315,7 @@ func (st *amSessionStore) Save(r *http.Request, w http.ResponseWriter, sess AmSe
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SessionInfo returns the number of anonymous sessions, all the session user names, and the current maximum number of sessions.
|
||||||
func (st *amSessionStore) SessionInfo() (int, []string, int) {
|
func (st *amSessionStore) SessionInfo() (int, []string, int) {
|
||||||
anons := 0
|
anons := 0
|
||||||
users := make([]string, 0, len(st.sessions))
|
users := make([]string, 0, len(st.sessions))
|
||||||
@@ -278,3 +369,51 @@ func (st *amSessionStore) sweep(tick <-chan time.Time, done chan bool) {
|
|||||||
}
|
}
|
||||||
done <- true
|
done <- true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// sessionStore is the global session store.
|
||||||
|
var sessionStore *amSessionStore
|
||||||
|
|
||||||
|
// SetupAmSessionManager sets up the session store and its sweeper goroutine.
|
||||||
|
func SetupAmSessionManager() func() {
|
||||||
|
// get the time for the session to expire
|
||||||
|
d, err := time.ParseDuration(config.GlobalConfig.Site.SessionExpire)
|
||||||
|
if err != nil {
|
||||||
|
d, err = time.ParseDuration("1h")
|
||||||
|
if err != nil {
|
||||||
|
panic(err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// create session store
|
||||||
|
sessionStore = createAmSessionStore(d)
|
||||||
|
|
||||||
|
// get the clock value to run sweeps
|
||||||
|
d, err = time.ParseDuration("1s")
|
||||||
|
if err != nil {
|
||||||
|
panic(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// set up the sweep runner
|
||||||
|
tkr := time.NewTicker(d)
|
||||||
|
done := make(chan bool)
|
||||||
|
go sessionStore.sweep(tkr.C, done)
|
||||||
|
return func() {
|
||||||
|
// stop the sweep runner
|
||||||
|
sessionStore.sweepRunning.Store(false)
|
||||||
|
<-done
|
||||||
|
tkr.Stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SessionStoreInjector is middleware that injects the session store into the context variables.
|
||||||
|
func SessionStoreInjector(next echo.HandlerFunc) echo.HandlerFunc {
|
||||||
|
return func(c echo.Context) error {
|
||||||
|
c.Set("AmSessionStore", sessionStore)
|
||||||
|
return next(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AmSessions returns the information about all current sessions.
|
||||||
|
func AmSessions() (int, []string, int) {
|
||||||
|
return sessionStore.SessionInfo()
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,306 +0,0 @@
|
|||||||
/*
|
|
||||||
* Amsterdam Web Communities System
|
|
||||||
* Copyright (c) 2025 Erbosoft Metaverse Design Solutions, All Rights Reserved
|
|
||||||
*
|
|
||||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Package ui holds the support for the Amsterdam user interface, wrapping Echo and Jet templates.
|
|
||||||
package ui
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/rand"
|
|
||||||
"encoding/gob"
|
|
||||||
"encoding/hex"
|
|
||||||
"net/http"
|
|
||||||
"slices"
|
|
||||||
"sync"
|
|
||||||
"sync/atomic"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.erbosoft.com/amy/amsterdam/config"
|
|
||||||
"git.erbosoft.com/amy/amsterdam/database"
|
|
||||||
"github.com/gorilla/sessions"
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
func AmSessionGet(sess *sessions.Session, key any) (any, bool) {
|
|
||||||
if sess == nil {
|
|
||||||
return 0, false
|
|
||||||
}
|
|
||||||
mtx := sess.Values["_mutex"].(*sync.RWMutex)
|
|
||||||
mtx.RLock()
|
|
||||||
defer mtx.RUnlock()
|
|
||||||
rc, ok := sess.Values[key]
|
|
||||||
return rc, ok
|
|
||||||
}
|
|
||||||
|
|
||||||
func AmSessionPut(sess *sessions.Session, key, value any) {
|
|
||||||
if sess != nil {
|
|
||||||
mtx := sess.Values["_mutex"].(*sync.RWMutex)
|
|
||||||
mtx.Lock()
|
|
||||||
defer mtx.Unlock()
|
|
||||||
sess.Values[key] = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func AmSessionErase(sess *sessions.Session) {
|
|
||||||
if sess != nil {
|
|
||||||
mtx := sess.Values["_mutex"].(*sync.RWMutex)
|
|
||||||
mtx.Lock()
|
|
||||||
defer mtx.Unlock()
|
|
||||||
for k := range sess.Values {
|
|
||||||
if k != "_mutex" {
|
|
||||||
delete(sess.Values, k)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// AmsterdamStore is our implewmentation of the Gorilla session store that works close to HttpSession in Java.
|
|
||||||
type AmsterdamStore struct {
|
|
||||||
mutex sync.RWMutex
|
|
||||||
sessions map[string]*sessions.Session
|
|
||||||
maxEntries int
|
|
||||||
expiry time.Duration
|
|
||||||
sweepRunning atomic.Bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func createAmsterdamStore(exp time.Duration) *AmsterdamStore {
|
|
||||||
rc := AmsterdamStore{
|
|
||||||
sessions: make(map[string]*sessions.Session),
|
|
||||||
maxEntries: 0,
|
|
||||||
expiry: exp,
|
|
||||||
}
|
|
||||||
rc.sweepRunning.Store(true)
|
|
||||||
return &rc
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Get (from Store interface) retrieves a new or existing session for the request.
|
|
||||||
* 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
/* New (from Store interface) creates and returns a new session object.
|
|
||||||
* 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
|
|
||||||
session.Values["_mutex"] = new(sync.RWMutex)
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Save (from Store interface) saves off the sessin information to the response.
|
|
||||||
* 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 {
|
|
||||||
cookie := sessions.NewCookie(session.Name(), session.ID, session.Options)
|
|
||||||
http.SetCookie(w, cookie)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
/* sweep sweeps sessions to remove expired ones.
|
|
||||||
* Parameters:
|
|
||||||
* tick - Channel that "pulses" periodically to run the task.
|
|
||||||
* done - Channel we write to when we're done.
|
|
||||||
*/
|
|
||||||
func (st *AmsterdamStore) sweep(tick <-chan time.Time, done chan bool) {
|
|
||||||
for range tick {
|
|
||||||
if st.sweepRunning.Load() {
|
|
||||||
// phase 1 - identify expired sessions
|
|
||||||
st.mutex.RLock()
|
|
||||||
zap := make([]string, 0, len(st.sessions))
|
|
||||||
for k, v := range st.sessions {
|
|
||||||
lastTime, ok := AmSessionGet(v, "lasthit")
|
|
||||||
if ok && time.Since(lastTime.(time.Time)) > st.expiry {
|
|
||||||
zap = append(zap, k)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
st.mutex.RUnlock()
|
|
||||||
|
|
||||||
// phase 2 - get rid of the expired sessions
|
|
||||||
for _, k := range zap {
|
|
||||||
st.mutex.Lock()
|
|
||||||
s, ok := st.sessions[k]
|
|
||||||
if ok {
|
|
||||||
delete(st.sessions, k)
|
|
||||||
AmSessionErase(s)
|
|
||||||
}
|
|
||||||
st.mutex.Unlock()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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 {
|
|
||||||
v, ok := AmSessionGet(s, "user_anon")
|
|
||||||
if ok && v.(bool) {
|
|
||||||
anons++
|
|
||||||
} else {
|
|
||||||
name, _ := AmSessionGet(s, "user_name")
|
|
||||||
users = append(users, 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.
|
|
||||||
func init() {
|
|
||||||
gob.Register(time.Time{})
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetupSessionManager sets up the session manager.
|
|
||||||
func SetupSessionManager() func() {
|
|
||||||
// get the time for the session to expire
|
|
||||||
d, err := time.ParseDuration(config.GlobalConfig.Site.SessionExpire)
|
|
||||||
if err != nil {
|
|
||||||
d, err = time.ParseDuration("1h")
|
|
||||||
if err != nil {
|
|
||||||
panic(err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// create session store
|
|
||||||
SessionStore = createAmsterdamStore(d)
|
|
||||||
|
|
||||||
// get the clock value to run sweeps
|
|
||||||
d, err = time.ParseDuration("1s")
|
|
||||||
if err != nil {
|
|
||||||
panic(err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
// set up the sweep runner
|
|
||||||
tkr := time.NewTicker(d)
|
|
||||||
done := make(chan bool)
|
|
||||||
go SessionStore.sweep(tkr.C, done)
|
|
||||||
return func() {
|
|
||||||
// stop the sweep runner
|
|
||||||
SessionStore.sweepRunning.Store(false)
|
|
||||||
<-done
|
|
||||||
tkr.Stop()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// AmSessionUid returns the current user ID of the session.
|
|
||||||
func AmSessionUid(session *sessions.Session) (int32, bool) {
|
|
||||||
rc, ok := AmSessionGet(session, "user_id")
|
|
||||||
if ok {
|
|
||||||
return rc.(int32), ok
|
|
||||||
} else {
|
|
||||||
return -1, ok
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* AmSetSessionUser sets the user for the session.
|
|
||||||
* Parameters:
|
|
||||||
* session - The session to be updated.
|
|
||||||
* user - The user to be associated with the session.
|
|
||||||
*/
|
|
||||||
func AmSetSessionUser(session *sessions.Session, user *database.User) {
|
|
||||||
AmSessionPut(session, "user_id", user.Uid)
|
|
||||||
AmSessionPut(session, "user_name", user.Username)
|
|
||||||
AmSessionPut(session, "user_anon", user.IsAnon)
|
|
||||||
}
|
|
||||||
|
|
||||||
// setSessionAnon sets the user for the session to the anonymous user.
|
|
||||||
func setSessionAnon(ctx context.Context, session *sessions.Session) {
|
|
||||||
u, err := database.AmGetAnonUser(ctx)
|
|
||||||
if err == nil {
|
|
||||||
AmSetSessionUser(session, u)
|
|
||||||
} else {
|
|
||||||
log.Errorf("unable to set anonymous user: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var lastHitMutex sync.Mutex
|
|
||||||
|
|
||||||
// AmSessionFirstTime initializes the session after it's first created.
|
|
||||||
func AmSessionFirstTime(ctx context.Context, session *sessions.Session) {
|
|
||||||
lastHitMutex.Lock()
|
|
||||||
setSessionAnon(ctx, session)
|
|
||||||
AmSessionPut(session, "lasthit", time.Now())
|
|
||||||
lastHitMutex.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
// AmResetSession clears the specified session.
|
|
||||||
func AmResetSession(ctx context.Context, session *sessions.Session) {
|
|
||||||
lastHitMutex.Lock()
|
|
||||||
AmSessionErase(session)
|
|
||||||
setSessionAnon(ctx, session)
|
|
||||||
AmSessionPut(session, "lasthit", time.Now())
|
|
||||||
lastHitMutex.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
// AmHitSession "hits" a session, updating its "last hit" time.
|
|
||||||
func AmHitSession(session *sessions.Session) {
|
|
||||||
lastHitMutex.Lock()
|
|
||||||
AmSessionPut(session, "lasthit", time.Now())
|
|
||||||
lastHitMutex.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
/* AmSessions returns information about the currently active sessions.
|
|
||||||
* Returns:
|
|
||||||
* Number of users active but not logged in
|
|
||||||
* List of user names currently logged in
|
|
||||||
* Maximum number of users ever in session table.
|
|
||||||
*/
|
|
||||||
func AmSessions() (int, []string, int) {
|
|
||||||
return SessionStore.sessionInfo()
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user