Skip to content

Commit

Permalink
feat: v3 refactor
Browse files Browse the repository at this point in the history
- Update store interface to take session ID instead of returning on
  Create()
- Introduce hooks to generate and validate session ID in manager. By
  default use alpha-numeric 32 length ID.
- Rename get and set cookie register method to `SetCookieHooks`.
- Ditch ErrFieldNotFound error and get/set supposed to return Nil
  instead.
  • Loading branch information
vividvilla committed May 29, 2024
1 parent 34a213a commit d88ef2e
Show file tree
Hide file tree
Showing 7 changed files with 332 additions and 191 deletions.
5 changes: 1 addition & 4 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
module github.com/vividvilla/simplesessions/v2

require (
github.com/stretchr/testify v1.9.0
github.com/valyala/fasthttp v1.40.0
)
require github.com/stretchr/testify v1.9.0

go 1.14
165 changes: 128 additions & 37 deletions manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ package simplesessions

import (
"context"
"crypto/rand"
"fmt"
"net/http"
"time"
"unicode"
)

type ctxNameType string
Expand All @@ -13,6 +15,12 @@ const (
// Default cookie name used to store session.
defaultCookieName = "session"

// Default cookie path.
defaultCookiePath = "/"

// default sessionID length.
defaultSessIDLength = 32

// ContextName is the key used to store session in context passed to acquire method.
ContextName ctxNameType = "_simple_session"
)
Expand All @@ -25,11 +33,17 @@ type Manager struct {
// Store basic cookie details.
opts *Options

// Callback to get http cookie.
getCookieCb func(name string, r interface{}) (*http.Cookie, error)
// Hook to get http cookie.
getCookieHook func(name string, r interface{}) (*http.Cookie, error)

// Hook to set http cookie.
setCookieHook func(cookie *http.Cookie, w interface{}) error

// generate cookie ID.
generateID func() (string, error)

// Callback to set http cookie.
setCookieCb func(cookie *http.Cookie, w interface{}) error
// validate cookie ID.
validateID func(string) bool
}

// Options are available options to configure Manager.
Expand All @@ -38,27 +52,45 @@ type Options struct {
// If disabled then new session can only be created using NewSession() method.
EnableAutoCreate bool

// CookieName sets http cookie name. This is also sent as cookie name in `GetCookie` callback.
CookieName string
// Cookie ID length. Defaults to alphanumeric 32 characters.
// Might not be applicable to some stores like SecureCookie.
// Also not applicable if custom generateID and validateID is set.
SessionIDLength int

// CookieDomain sets hostname for the cookie. Domain specifies allowed hosts to receive the cookie.
CookieDomain string
// Cookie options.
Cookie CookieOptions
}

type CookieOptions struct {
// Name sets http cookie name. This is also sent as cookie name in `GetCookie` callback.
Name string

// CookiePath sets path for the cookie. Path indicates a URL path that must exist in the requested URL in order to send the cookie header.
CookiePath string
// Domain sets hostname for the cookie. Domain specifies allowed hosts to receive the cookie.
Domain string

// IsSecureCookie marks the cookie as secure cookie (only sent in HTTPS).
IsSecureCookie bool
// Path sets path for the cookie. Path indicates a URL path that must exist in the requested URL in order to send the cookie header.
Path string

// IsHTTPOnlyCookie marks the cookie as http only cookie. JS won't be able to access the cookie so prevents XSS attacks.
IsHTTPOnlyCookie bool
// IsSecure marks the cookie as secure cookie (only sent in HTTPS).
IsSecure bool

// CookieLifeTime sets expiry time for cookie.
// If expiry time is not specified then cookie is set as session cookie which is cleared on browser close.
CookieLifetime time.Duration
// IsHTTPOnly marks the cookie as http only cookie. JS won't be able to access the cookie so prevents XSS attacks.
IsHTTPOnly bool

// SameSite sets allows you to declare if your cookie should be restricted to a first-party or same-site context.
SameSite http.SameSite

// Expires sets absolute expiration date and time for the cookie.
// If both Expires and MaxAge are sent then MaxAge takes precedence over Expires.
// Cookies without a Max-age or Expires attribute – are deleted when the current session ends
// and some browsers use session restoring when restarting. This can cause session cookies to last indefinitely.
Expires time.Time

// Sets the cookie's expiration in seconds from the current time, internally its rounder off to nearest seconds.
// If both Expires and MaxAge are sent then MaxAge takes precedence over Expires.
// Cookies without a Max-age or Expires attribute – are deleted when the current session ends
// and some browsers use session restoring when restarting. This can cause session cookies to last indefinitely.
MaxAge time.Duration
}

// New creates a new session manager for given options.
Expand All @@ -68,15 +100,23 @@ func New(opts Options) *Manager {
}

// Set default cookie name if not set
if m.opts.CookieName == "" {
m.opts.CookieName = defaultCookieName
if m.opts.Cookie.Name == "" {
m.opts.Cookie.Name = defaultCookieName
}

// If path not given then set to root path
if m.opts.CookiePath == "" {
m.opts.CookiePath = "/"
if m.opts.Cookie.Path == "" {
m.opts.Cookie.Path = defaultCookiePath
}

if m.opts.SessionIDLength == 0 {
m.opts.SessionIDLength = defaultSessIDLength
}

// Assign default set and validate generate ID.
m.generateID = m.defaultGenerateID
m.validateID = m.defaultValidateID

return m
}

Expand All @@ -85,35 +125,52 @@ func (m *Manager) UseStore(str Store) {
m.store = str
}

// RegisterGetCookie sets a callback to retrieve an HTTP cookie during session acquisition.
func (m *Manager) RegisterGetCookie(cb func(string, interface{}) (*http.Cookie, error)) {
m.getCookieCb = cb
// SetCookieHooks cane be used to get and set HTTP cookie for the session.
//
// getCookie hook takes session ID and reader interface and returns http.Cookie and error.
// In a HTTP request context reader interface will be the http request object and
// it should obtain http.Cookie from the request object for the given cookie ID.
//
// setCookie hook takes http.Cookie object and a writer interface and returns error.
// In a HTTP request context the write interface will be the http request object and
// it should write http request with the incoming cookie.
func (m *Manager) SetCookieHooks(getCookie func(string, interface{}) (*http.Cookie, error), setCookie func(*http.Cookie, interface{}) error) {
m.getCookieHook = getCookie
m.setCookieHook = setCookie
}

// RegisterSetCookie sets a callback to set an HTTP cookie during session acquisition.
func (m *Manager) RegisterSetCookie(cb func(*http.Cookie, interface{}) error) {
m.setCookieCb = cb
// SetSessionIDHooks cane be used to generate and validate custom session ID.
// Bydefault alpha-numeric 32bit length session ID is used if its not set.
// - Generating custom session ID, which will be uses as the ID for storing sessions in the backend.
// - Validating custom session ID, which will be used to verify the ID before querying backend.
func (m *Manager) SetSessionIDHooks(generateID func() (string, error), validateID func(string) bool) {
m.generateID = generateID
m.validateID = validateID
}

// NewSession creates a new `Session` and updates the cookie with a new session ID,
// replacing any existing session ID if it exists.
func (m *Manager) NewSession(r, w interface{}) (*Session, error) {
// Check if any store is set
if m.store == nil {
return nil, fmt.Errorf("session store is not set")
return nil, fmt.Errorf("session store not set")
}

if m.setCookieCb == nil {
return nil, fmt.Errorf("callback `SetCookie` not set")
if m.setCookieHook == nil {
return nil, fmt.Errorf("`SetCookie` hook not set")
}

// Create new cookie in store and write to front.
// Store also calls `WriteCookie`` to write to http interface.
id, err := m.store.Create()
id, err := m.generateID()
if err != nil {
return nil, errAs(err)
}

if err = m.store.Create(id); err != nil {
return nil, errAs(err)
}

var sess = &Session{
id: id,
manager: m,
Expand All @@ -137,16 +194,16 @@ func (m *Manager) NewSession(r, w interface{}) (*Session, error) {
func (m *Manager) Acquire(c context.Context, r, w interface{}) (*Session, error) {
// Check if any store is set
if m.store == nil {
return nil, fmt.Errorf("session store is not set")
return nil, fmt.Errorf("session store not set")
}

// Check if callbacks are set
if m.getCookieCb == nil {
return nil, fmt.Errorf("callback `GetCookie` not set")
if m.getCookieHook == nil {
return nil, fmt.Errorf("`GetCookie` hook not set")
}

if m.setCookieCb == nil {
return nil, fmt.Errorf("callback `SetCookie` not set")
if m.setCookieHook == nil {
return nil, fmt.Errorf("`SetCookie` hook not set")
}

// If a session was already set in the context by a middleware somewhere, return that.
Expand All @@ -159,7 +216,7 @@ func (m *Manager) Acquire(c context.Context, r, w interface{}) (*Session, error)
// Get existing HTTP session cookie.
// If there's no error and there's a session ID (unvalidated at this point),
// return a session object.
ck, err := m.getCookieCb(m.opts.CookieName, r)
ck, err := m.getCookieHook(m.opts.Cookie.Name, r)
if err == nil && ck != nil && ck.Value != "" {
return &Session{
manager: m,
Expand All @@ -177,3 +234,37 @@ func (m *Manager) Acquire(c context.Context, r, w interface{}) (*Session, error)

return m.NewSession(r, w)
}

// defaultGenerateID generates a random alpha-num session ID.
// This will be the default method to generate cookie ID and
// can override using `SetCookieIDGenerate` method.
func (m *Manager) defaultGenerateID() (string, error) {
const dict = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
bytes := make([]byte, m.opts.SessionIDLength)
if _, err := rand.Read(bytes); err != nil {
return "", err
}

for k, v := range bytes {
bytes[k] = dict[v%byte(len(dict))]
}

return string(bytes), nil
}

// defaultValidateID validates the incoming to ID to check
// if its alpha-numeric with configured cookie ID length.
// Can override using `SetCookieIDGenerate` method.
func (m *Manager) defaultValidateID(id string) bool {
if len(id) != m.opts.SessionIDLength {
return false
}

for _, r := range id {
if !unicode.IsDigit(r) && !unicode.IsLetter(r) {
return false
}
}

return true
}
Loading

0 comments on commit d88ef2e

Please sign in to comment.