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:
parent
5124e6f8f7
commit
5e3dfacc80
@ -9,6 +9,7 @@ import fpmime "git.tebibyte.media/sashakoshka/step/providers/mime"
|
||||
import fpsprig "git.tebibyte.media/sashakoshka/step/providers/sprig"
|
||||
import fpimport "git.tebibyte.media/sashakoshka/step/providers/import"
|
||||
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 fpmarkdown "git.tebibyte.media/sashakoshka/step/providers/markdown"
|
||||
|
||||
@ -22,6 +23,7 @@ func All () []step.Provider {
|
||||
new(fpmime.Provider),
|
||||
new(fpsprig.Provider),
|
||||
new(fpimport.Provider),
|
||||
new(fpsession.Provider),
|
||||
new(fpstrings.Provider),
|
||||
new(fpvalidate.Provider),
|
||||
fpmarkdown.Default(),
|
||||
|
219
providers/session/session.go
Normal file
219
providers/session/session.go
Normal 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())
|
||||
}
|
Loading…
Reference in New Issue
Block a user