providers: Incomplete session support

Interestingly, attempting to compile and run ./cmd/stepd crashes
the Go compiler (Chimera, v1.23.3). Unknown line number, nil panic.
This commit is contained in:
Sasha Koshka 2024-12-13 22:11:24 -05:00
parent 5124e6f8f7
commit 5e3dfacc80
2 changed files with 221 additions and 0 deletions

View File

@ -9,6 +9,7 @@ import fpmime "git.tebibyte.media/sashakoshka/step/providers/mime"
import fpsprig "git.tebibyte.media/sashakoshka/step/providers/sprig" import fpsprig "git.tebibyte.media/sashakoshka/step/providers/sprig"
import fpimport "git.tebibyte.media/sashakoshka/step/providers/import" import fpimport "git.tebibyte.media/sashakoshka/step/providers/import"
import fpstrings "git.tebibyte.media/sashakoshka/step/providers/strings" import fpstrings "git.tebibyte.media/sashakoshka/step/providers/strings"
import fpsession "git.tebibyte.media/sashakoshka/step/providers/session"
import fpvalidate "git.tebibyte.media/sashakoshka/step/providers/validate" import fpvalidate "git.tebibyte.media/sashakoshka/step/providers/validate"
import fpmarkdown "git.tebibyte.media/sashakoshka/step/providers/markdown" import fpmarkdown "git.tebibyte.media/sashakoshka/step/providers/markdown"
@ -22,6 +23,7 @@ func All () []step.Provider {
new(fpmime.Provider), new(fpmime.Provider),
new(fpsprig.Provider), new(fpsprig.Provider),
new(fpimport.Provider), new(fpimport.Provider),
new(fpsession.Provider),
new(fpstrings.Provider), new(fpstrings.Provider),
new(fpvalidate.Provider), new(fpvalidate.Provider),
fpmarkdown.Default(), fpmarkdown.Default(),

View File

@ -0,0 +1,219 @@
package os
import "log"
import "time"
import "net/http"
import "html/template"
import "github.com/google/uuid"
import "git.tebibyte.media/sashakoshka/step"
import "git.tebibyte.media/sashakoshka/goutil/sync"
import "git.tebibyte.media/sashakoshka/goutil/container"
import shttp "git.tebibyte.media/sashakoshka/step/http"
const sessionIDCookieName = "step-session-id"
const defaultLifetime = 48 * time.Hour
var _ step.FuncProviderFor = new(Provider)
var _ step.Initializer = new(Provider)
var _ step.Configurable = new(Provider)
var _ step.Trimmer = new(Provider)
// Provider provides session functions.
type Provider struct {
Lifetime time.Duration
sessions usync.RWLocker[sessionMap]
}
// Package fulfills the step.Provider interface.
func (this *Provider) Package () string {
return "session"
}
func (this *Provider) Init () error {
this.sessions = usync.NewRWLocker(make(sessionMap))
return nil
}
func (this *Provider) Trim () {
sessions, done := this.sessions.Borrow()
defer done()
stale := ucontainer.Set[uuid.UUID] { }
for id, session := range sessions {
if session.Expired() {
stale.Add(id)
}
}
for id := range stale {
delete(sessions, id)
}
}
func (this *Provider) Configure (config step.Meta) error {
if lifetimeStr := config.Get("session.lifetime"); lifetimeStr != "" {
lifetime, err := time.ParseDuration(lifetimeStr)
if err != nil { return err }
this.Lifetime = lifetime
}
return nil
}
// FuncMapFor fulfills the step.FuncProviderFor interface.
func (this *Provider) FuncMapFor (document *step.Document) template.FuncMap {
stat := &state {
document: document,
sessions: &this.sessions,
}
return template.FuncMap {
"sessionHTTP": stat.funcSessionHTTP,
"session": stat.funcSession,
}
}
type state struct {
document *step.Document
sessions *usync.RWLocker[sessionMap]
lifetime time.Duration
}
func (this *state) funcSessionHTTP (
res shttp.WrappedResponseWriter,
req *http.Request,
) (
*Session,
error,
) {
var id uuid.UUID
if cookie, err := req.Cookie(sessionIDCookieName); err == nil {
if parsed, err := uuid.Parse(cookie.Value); err == nil {
id = parsed
}
}
var expiration time.Time
if this.lifetime == 0 {
expiration = time.Now().Add(defaultLifetime)
} else {
expiration = time.Now().Add(this.lifetime)
}
var result *session
if session, ok := this.checkSession(id); ok {
if this.lifetime != 0 {
session.setExpiration(expiration)
}
result = session
} else {
session, err := this.startSession(expiration)
if err != nil { return nil, err }
result = session
}
cookie := &http.Cookie {
Name: sessionIDCookieName,
Value: result.ID().String(),
Expires: expiration,
}
http.SetCookie(shttp.UnderlyingResponseWriter(res), cookie)
return result, nil
}
func (this *state) funcSession (id uuid.UUID) (*session, error) {
expires := time.Now().Add(this.lifetime)
if session, ok := this.checkSession(id); ok {
session.setExpiration(expires)
return session, nil
}
return this.startSession(expires)
}
func (this *state) checkSession (id uuid.UUID) (*session, bool) {
sessions, done := this.sessions.RBorrow()
defer done()
session, ok := sessions[id]
log.Println(ok)
if !ok || session.Expired() { return nil, false }
return session, true
}
func (this *state) startSession (expires time.Time) (*session, error) {
sessions, done := this.sessions.Borrow()
defer done()
id := uuid.New()
// air defense
if _, exists := sessions[id]; exists { return nil, step.ErrPigsFlying }
session := &Session {
parent: this,
id: id,
data: usync.NewRWLocker(make(map[string] any)),
expires: usync.NewRWLocker(expires),
}
sessions[id] = session
log.Println(id)
return session, nil
}
type sessionMap = map[uuid.UUID] *Session
type Session struct {
parent *state // immutable
id uuid.UUID // immutable
data usync.RWLocker[map[string] any] // mutable
expires usync.RWLocker[time.Time] // mutable
}
func (this *Session) ID () uuid.UUID {
return this.id
}
func (this *Session) Get (name string) any {
data, done := this.data.RBorrow()
defer done()
return data[name]
}
func (this *Session) Set (name string, value any) string {
data, done := this.data.Borrow()
defer done()
data[name] = value
return ""
}
func (this *Session) Del (name string) string {
data, done := this.data.Borrow()
defer done()
delete(data, name)
return ""
}
func (this *Session) Clear () string {
data, done := this.data.Borrow()
defer done()
clear(data)
return ""
}
func (this *Session) Close () (string, error) {
sessions, done := this.parent.sessions.Borrow()
defer done()
if _, ok := sessions[this.id]; ok {
return "", step.ErrDoubleClose
}
delete(sessions, this.id)
this.Clear()
return "", nil
}
func (this *Session) Expiration () time.Time {
expiration, done := this.expires.RBorrow()
defer done()
return expiration // ok because its not a pointer
}
func (this *Session) setExpiration (expires time.Time) string {
// not public because this only sets the expiration time of the session,
// and not the session cookie it represents because that would require
// breaking into the user's computer.
this.expires.Set(expires)
return ""
}
func (this *Session) Expired () bool {
return this.Expiration().After(time.Now())
}