go-macaron_session/session.go
Andrew Thornton b8f62d7880 Add samesite cookie options (#16)
Add samesite cookie options

Signed-off-by: Andrew Thornton <art27@cantab.net>

Securely encrypt flash cookie to prevent javascript attack

Signed-off-by: Andrew Thornton <art27@cantab.net>

Reviewed-on: macaron/session#16
Reviewed-by: silverwind <silverwind@noreply.gitea.io>
Reviewed-by: 6543 <6543@obermui.de>
Co-Authored-By: Andrew Thornton <art27@cantab.net>
Co-Committed-By: Andrew Thornton <art27@cantab.net>
2020-11-12 12:27:16 +08:00

431 lines
12 KiB
Go

// Copyright 2013 Beego Authors
// Copyright 2014 The Macaron Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"): you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.
// Package session a middleware that provides the session management of Macaron.
package session
import (
"encoding/hex"
"errors"
"fmt"
"net/http"
"net/url"
"strconv"
"time"
"gitea.com/macaron/macaron"
"gitea.com/macaron/macaron/cookie"
)
const _VERSION = "0.6.0"
func Version() string {
return _VERSION
}
// RawStore is the interface that operates the session data.
type RawStore interface {
// Set sets value to given key in session.
Set(interface{}, interface{}) error
// Get gets value by given key in session.
Get(interface{}) interface{}
// Delete deletes a key from session.
Delete(interface{}) error
// ID returns current session ID.
ID() string
// Release releases session resource and save data to provider.
Release() error
// Flush deletes all session data.
Flush() error
}
// Store is the interface that contains all data for one session process with specific ID.
type Store interface {
RawStore
// Read returns raw session store by session ID.
Read(string) (RawStore, error)
// Destroy deletes a session.
Destroy(*macaron.Context) error
// RegenerateId regenerates a session store from old session ID to new one.
RegenerateId(*macaron.Context) (RawStore, error)
// Count counts and returns number of sessions.
Count() int
// GC calls GC to clean expired sessions.
GC()
}
type store struct {
RawStore
*Manager
}
var _ Store = &store{}
// Options represents a struct for specifying configuration options for the session middleware.
type Options struct {
// Name of provider. Default is "memory".
Provider string
// Provider configuration, it's corresponding to provider.
ProviderConfig string
// Cookie name to save session ID. Default is "MacaronSession".
CookieName string
// Cookie path to store. Default is "/".
CookiePath string
// GC interval time in seconds. Default is 3600.
Gclifetime int64
// Max life time in seconds. Default is whatever GC interval time is.
Maxlifetime int64
// Use HTTPS only. Default is false.
Secure bool
// Cookie life time. Default is 0.
CookieLifeTime int
// SameSite set the cookie SameSite
SameSite http.SameSite
// Cookie domain name. Default is empty.
Domain string
// Session ID length. Default is 16.
IDLength int
// Configuration section name. Default is "session".
Section string
// Ignore release for websocket. Default is false.
IgnoreReleaseForWebSocket bool
// FlashEncryptionKey sets the encryption key for flash messages
FlashEncryptionKey string
}
func prepareOptions(options []Options) Options {
var opt Options
if len(options) > 0 {
opt = options[0]
}
if len(opt.Section) == 0 {
opt.Section = "session"
}
sec := macaron.Config().Section(opt.Section)
if len(opt.Provider) == 0 {
opt.Provider = sec.Key("PROVIDER").MustString("memory")
}
if len(opt.ProviderConfig) == 0 {
opt.ProviderConfig = sec.Key("PROVIDER_CONFIG").MustString("data/sessions")
}
if len(opt.CookieName) == 0 {
opt.CookieName = sec.Key("COOKIE_NAME").MustString("MacaronSession")
}
if len(opt.CookiePath) == 0 {
opt.CookiePath = sec.Key("COOKIE_PATH").MustString("/")
}
if opt.Gclifetime == 0 {
opt.Gclifetime = sec.Key("GC_INTERVAL_TIME").MustInt64(3600)
}
if opt.Maxlifetime == 0 {
opt.Maxlifetime = sec.Key("MAX_LIFE_TIME").MustInt64(opt.Gclifetime)
}
if !opt.Secure {
opt.Secure = sec.Key("SECURE").MustBool()
}
if opt.CookieLifeTime == 0 {
opt.CookieLifeTime = sec.Key("COOKIE_LIFE_TIME").MustInt()
}
if opt.SameSite == 0 {
opt.SameSite = http.SameSite(sec.Key("SAME_SITE").MustInt())
}
if len(opt.Domain) == 0 {
opt.Domain = sec.Key("DOMAIN").String()
}
if opt.IDLength == 0 {
opt.IDLength = sec.Key("ID_LENGTH").MustInt(16)
}
if !opt.IgnoreReleaseForWebSocket {
opt.IgnoreReleaseForWebSocket = sec.Key("IGNORE_RELEASE_FOR_WEBSOCKET").MustBool()
}
if len(opt.FlashEncryptionKey) == 0 {
opt.FlashEncryptionKey = sec.Key("FLASH_ENCRYPTION_KEY").MustString("")
}
if len(opt.FlashEncryptionKey) == 0 {
opt.FlashEncryptionKey, _ = NewSecret()
}
return opt
}
// Sessioner is a middleware that maps a session.SessionStore service into the Macaron handler chain.
// An single variadic session.Options struct can be optionally provided to configure.
func Sessioner(options ...Options) macaron.Handler {
opt := prepareOptions(options)
manager, err := NewManager(opt.Provider, opt)
if err != nil {
panic(err)
}
go manager.startGC()
return func(ctx *macaron.Context) {
sess, err := manager.Start(ctx)
if err != nil {
panic("session(start): " + err.Error())
}
// Get flash.
flashCookie := ctx.GetCookie("macaron_flash")
decrypted, _ := DecryptSecret(opt.FlashEncryptionKey, flashCookie)
vals, _ := url.ParseQuery(decrypted)
if len(vals) > 0 {
f := &Flash{Values: vals}
f.ErrorMsg = f.Get("error")
f.SuccessMsg = f.Get("success")
f.InfoMsg = f.Get("info")
f.WarningMsg = f.Get("warning")
t, _ := strconv.ParseInt(f.Get("time"), 10, 64)
now := time.Now().Unix()
if now-t > 0 && now-t < 3600 {
ctx.Data["Flash"] = f
}
}
if len(flashCookie) > 0 {
ctx.SetCookie("macaron_flash", "", -1, opt.CookiePath,
cookie.Domain(opt.Domain),
cookie.HTTPOnly(true),
cookie.Secure(opt.Secure),
cookie.SameSite(opt.SameSite))
}
f := &Flash{ctx, url.Values{}, "", "", "", ""}
ctx.Resp.Before(func(macaron.ResponseWriter) {
f.Set("time", strconv.FormatInt(time.Now().Unix(), 10))
if flash := f.Encode(); len(flash) > 0 {
encrypted, err := EncryptSecret(opt.FlashEncryptionKey, flash)
if err == nil {
ctx.SetCookie("macaron_flash", encrypted, 0, opt.CookiePath,
cookie.Domain(opt.Domain),
cookie.HTTPOnly(true),
cookie.Secure(opt.Secure),
cookie.SameSite(opt.SameSite))
}
}
})
ctx.Map(f)
s := store{
RawStore: sess,
Manager: manager,
}
ctx.MapTo(s, (*Store)(nil))
ctx.Next()
if manager.opt.IgnoreReleaseForWebSocket && ctx.Req.Header.Get("Upgrade") == "websocket" {
return
}
if err = sess.Release(); err != nil {
panic("session(release): " + err.Error())
}
}
}
// Provider is the interface that provides session manipulations.
type Provider interface {
// Init initializes session provider.
Init(gclifetime int64, config string) error
// Read returns raw session store by session ID.
Read(sid string) (RawStore, error)
// Exist returns true if session with given ID exists.
Exist(sid string) bool
// Destroy deletes a session by session ID.
Destroy(sid string) error
// Regenerate regenerates a session store from old session ID to new one.
Regenerate(oldsid, sid string) (RawStore, error)
// Count counts and returns number of sessions.
Count() int
// GC calls GC to clean expired sessions.
GC()
}
var providers = make(map[string]Provider)
// Register registers a provider.
func Register(name string, provider Provider) {
if provider == nil {
panic("session: cannot register provider with nil value")
}
if _, dup := providers[name]; dup {
panic(fmt.Errorf("session: cannot register provider '%s' twice", name))
}
providers[name] = provider
}
// _____
// / \ _____ ____ _____ ____ ___________
// / \ / \\__ \ / \\__ \ / ___\_/ __ \_ __ \
// / Y \/ __ \| | \/ __ \_/ /_/ > ___/| | \/
// \____|__ (____ /___| (____ /\___ / \___ >__|
// \/ \/ \/ \//_____/ \/
// Manager represents a struct that contains session provider and its configuration.
type Manager struct {
provider Provider
opt Options
}
// NewManager creates and returns a new session manager by given provider name and configuration.
// It panics when given provider isn't registered.
func NewManager(name string, opt Options) (*Manager, error) {
p, ok := providers[name]
if !ok {
return nil, fmt.Errorf("session: unknown provider '%s'(forgotten import?)", name)
}
return &Manager{p, opt}, p.Init(opt.Maxlifetime, opt.ProviderConfig)
}
// sessionID generates a new session ID with rand string, unix nano time, remote addr by hash function.
func (m *Manager) sessionID() string {
return hex.EncodeToString(generateRandomKey(m.opt.IDLength / 2))
}
// validSessionID tests whether a provided session ID is a valid session ID.
func (m *Manager) validSessionID(sid string) (bool, error) {
if len(sid) != m.opt.IDLength {
return false, errors.New("invalid 'sid': " + sid)
}
for i := range sid {
switch {
case '0' <= sid[i] && sid[i] <= '9':
case 'a' <= sid[i] && sid[i] <= 'f':
default:
return false, errors.New("invalid 'sid': " + sid)
}
}
return true, nil
}
// Start starts a session by generating new one
// or retrieve existence one by reading session ID from HTTP request if it's valid.
func (m *Manager) Start(ctx *macaron.Context) (RawStore, error) {
sid := ctx.GetCookie(m.opt.CookieName)
valid, _ := m.validSessionID(sid)
if len(sid) > 0 && valid && m.provider.Exist(sid) {
return m.provider.Read(sid)
}
sid = m.sessionID()
sess, err := m.provider.Read(sid)
if err != nil {
return nil, err
}
cookie := &http.Cookie{
Name: m.opt.CookieName,
Value: sid,
Path: m.opt.CookiePath,
HttpOnly: true,
Secure: m.opt.Secure,
Domain: m.opt.Domain,
SameSite: m.opt.SameSite,
}
if m.opt.CookieLifeTime >= 0 {
cookie.MaxAge = m.opt.CookieLifeTime
}
http.SetCookie(ctx.Resp, cookie)
ctx.Req.AddCookie(cookie)
return sess, nil
}
// Read returns raw session store by session ID.
func (m *Manager) Read(sid string) (RawStore, error) {
// Ensure we're trying to read a valid session ID
if _, err := m.validSessionID(sid); err != nil {
return nil, err
}
return m.provider.Read(sid)
}
// Destroy deletes a session by given ID.
func (m *Manager) Destroy(ctx *macaron.Context) error {
sid := ctx.GetCookie(m.opt.CookieName)
if len(sid) == 0 {
return nil
}
if _, err := m.validSessionID(sid); err != nil {
return err
}
if err := m.provider.Destroy(sid); err != nil {
return err
}
cookie := &http.Cookie{
Name: m.opt.CookieName,
Path: m.opt.CookiePath,
HttpOnly: true,
Expires: time.Now(),
MaxAge: -1,
}
http.SetCookie(ctx.Resp, cookie)
return nil
}
// RegenerateId regenerates a session store from old session ID to new one.
func (m *Manager) RegenerateId(ctx *macaron.Context) (sess RawStore, err error) {
sid := m.sessionID()
oldsid := ctx.GetCookie(m.opt.CookieName)
_, err = m.validSessionID(oldsid)
if err != nil {
return nil, err
}
sess, err = m.provider.Regenerate(oldsid, sid)
if err != nil {
return nil, err
}
cookie := &http.Cookie{
Name: m.opt.CookieName,
Value: sid,
Path: m.opt.CookiePath,
HttpOnly: true,
Secure: m.opt.Secure,
Domain: m.opt.Domain,
SameSite: m.opt.SameSite,
}
if m.opt.CookieLifeTime >= 0 {
cookie.MaxAge = m.opt.CookieLifeTime
}
http.SetCookie(ctx.Resp, cookie)
ctx.Req.AddCookie(cookie)
return sess, nil
}
// Count counts and returns number of sessions.
func (m *Manager) Count() int {
return m.provider.Count()
}
// GC starts GC job in a certain period.
func (m *Manager) GC() {
m.provider.GC()
}
// startGC starts GC job in a certain period.
func (m *Manager) startGC() {
m.GC()
time.AfterFunc(time.Duration(m.opt.Gclifetime)*time.Second, func() { m.startGC() })
}
// SetSecure indicates whether to set cookie with HTTPS or not.
func (m *Manager) SetSecure(secure bool) {
m.opt.Secure = secure
}