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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user