Added challenge middleware
This commit is contained in:
parent
6b1a008719
commit
1e55797ec8
7
go.mod
7
go.mod
|
@ -73,11 +73,15 @@ require (
|
||||||
go.uber.org/zap v1.22.0
|
go.uber.org/zap v1.22.0
|
||||||
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e
|
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e
|
||||||
golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb
|
golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb
|
||||||
|
google.golang.org/protobuf v1.28.0
|
||||||
|
gopkg.in/go-playground/assert.v1 v1.2.1 // indirect
|
||||||
gopkg.in/go-playground/validator.v9 v9.31.0
|
gopkg.in/go-playground/validator.v9 v9.31.0
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0
|
gopkg.in/natefinch/lumberjack.v2 v2.0.0
|
||||||
olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3
|
olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3
|
||||||
)
|
)
|
||||||
|
|
||||||
|
require github.com/gorilla/sessions v1.2.1
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/PuerkitoBio/goquery v1.6.1 // indirect
|
github.com/PuerkitoBio/goquery v1.6.1 // indirect
|
||||||
github.com/RoaringBitmap/roaring v0.9.4 // indirect
|
github.com/RoaringBitmap/roaring v0.9.4 // indirect
|
||||||
|
@ -135,6 +139,7 @@ require (
|
||||||
github.com/golang/snappy v0.0.4 // indirect
|
github.com/golang/snappy v0.0.4 // indirect
|
||||||
github.com/google/btree v1.0.1 // indirect
|
github.com/google/btree v1.0.1 // indirect
|
||||||
github.com/google/gopacket v1.1.19 // indirect
|
github.com/google/gopacket v1.1.19 // indirect
|
||||||
|
github.com/gorilla/securecookie v1.1.1 // indirect
|
||||||
github.com/gorilla/websocket v1.5.0 // indirect
|
github.com/gorilla/websocket v1.5.0 // indirect
|
||||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||||
|
@ -254,8 +259,6 @@ require (
|
||||||
golang.org/x/time v0.0.0-20220224211638-0e9765cccd65 // indirect
|
golang.org/x/time v0.0.0-20220224211638-0e9765cccd65 // indirect
|
||||||
golang.org/x/tools v0.1.11 // indirect
|
golang.org/x/tools v0.1.11 // indirect
|
||||||
golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f // indirect
|
golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f // indirect
|
||||||
google.golang.org/protobuf v1.28.0 // indirect
|
|
||||||
gopkg.in/go-playground/assert.v1 v1.2.1 // indirect
|
|
||||||
gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce // indirect
|
gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce // indirect
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
|
||||||
gopkg.in/urfave/cli.v1 v1.20.0 // indirect
|
gopkg.in/urfave/cli.v1 v1.20.0 // indirect
|
||||||
|
|
4
go.sum
4
go.sum
|
@ -1056,6 +1056,10 @@ github.com/gorilla/mux v1.7.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2z
|
||||||
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||||
github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||||
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||||
|
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
|
||||||
|
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||||
|
github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
|
||||||
|
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||||
github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||||
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
|
|
|
@ -9,18 +9,22 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/http/cookiejar"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcutil/base58"
|
||||||
)
|
)
|
||||||
|
|
||||||
type PairingClient struct {
|
type PairingClient struct {
|
||||||
*http.Client
|
*http.Client
|
||||||
PayloadManager
|
PayloadManager
|
||||||
|
|
||||||
baseAddress *url.URL
|
baseAddress *url.URL
|
||||||
certPEM []byte
|
certPEM []byte
|
||||||
serverPK *ecdsa.PublicKey
|
serverPK *ecdsa.PublicKey
|
||||||
serverMode Mode
|
serverMode Mode
|
||||||
serverCert *x509.Certificate
|
serverCert *x509.Certificate
|
||||||
|
serverChallenge []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewPairingClient(c *ConnectionParams, config *PairingPayloadManagerConfig) (*PairingClient, error) {
|
func NewPairingClient(c *ConnectionParams, config *PairingPayloadManagerConfig) (*PairingClient, error) {
|
||||||
|
@ -55,13 +59,18 @@ func NewPairingClient(c *ConnectionParams, config *PairingPayloadManagerConfig)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cj, err := cookiejar.New(nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
pm, err := NewPairingPayloadManager(c.aesKey, config)
|
pm, err := NewPairingPayloadManager(c.aesKey, config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &PairingClient{
|
return &PairingClient{
|
||||||
Client: &http.Client{Transport: tr},
|
Client: &http.Client{Transport: tr, Jar: cj},
|
||||||
baseAddress: u,
|
baseAddress: u,
|
||||||
certPEM: certPem,
|
certPEM: certPem,
|
||||||
serverCert: serverCert,
|
serverCert: serverCert,
|
||||||
|
@ -76,6 +85,10 @@ func (c *PairingClient) PairAccount() error {
|
||||||
case Receiving:
|
case Receiving:
|
||||||
return c.sendAccountData()
|
return c.sendAccountData()
|
||||||
case Sending:
|
case Sending:
|
||||||
|
err := c.getChallenge()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
return c.receiveAccountData()
|
return c.receiveAccountData()
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("unrecognised server mode '%d'", c.serverMode)
|
return fmt.Errorf("unrecognised server mode '%d'", c.serverMode)
|
||||||
|
@ -84,21 +97,43 @@ func (c *PairingClient) PairAccount() error {
|
||||||
|
|
||||||
func (c *PairingClient) sendAccountData() error {
|
func (c *PairingClient) sendAccountData() error {
|
||||||
c.baseAddress.Path = pairingReceive
|
c.baseAddress.Path = pairingReceive
|
||||||
_, err := c.Post(c.baseAddress.String(), "application/octet-stream", bytes.NewBuffer(c.PayloadManager.ToSend()))
|
resp, err := c.Post(c.baseAddress.String(), "application/octet-stream", bytes.NewBuffer(c.PayloadManager.ToSend()))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return fmt.Errorf("status not ok, received '%s'", resp.Status)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *PairingClient) receiveAccountData() error {
|
func (c *PairingClient) receiveAccountData() error {
|
||||||
c.baseAddress.Path = pairingSend
|
c.baseAddress.Path = pairingSend
|
||||||
resp, err := c.Get(c.baseAddress.String())
|
req, err := http.NewRequest(http.MethodGet, c.baseAddress.String(), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if c.serverChallenge != nil {
|
||||||
|
ec, err := c.PayloadManager.EncryptPlain(c.serverChallenge)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set(sessionChallenge, base58.Encode(ec))
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := c.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return fmt.Errorf("status not ok, received '%s'", resp.Status)
|
||||||
|
}
|
||||||
|
|
||||||
payload, err := ioutil.ReadAll(resp.Body)
|
payload, err := ioutil.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -106,3 +141,18 @@ func (c *PairingClient) receiveAccountData() error {
|
||||||
|
|
||||||
return c.PayloadManager.Receive(payload)
|
return c.PayloadManager.Receive(payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *PairingClient) getChallenge() error {
|
||||||
|
c.baseAddress.Path = pairingChallenge
|
||||||
|
resp, err := c.Get(c.baseAddress.String())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return fmt.Errorf("status not ok, received '%s'", resp.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.serverChallenge, err = ioutil.ReadAll(resp.Body)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
|
@ -116,7 +116,7 @@ func (tpsc *TestPairingServerComponents) SetupPairingServerComponents(t *testing
|
||||||
}
|
}
|
||||||
|
|
||||||
type MockEncryptOnlyPayloadManager struct {
|
type MockEncryptOnlyPayloadManager struct {
|
||||||
pem *PayloadEncryptionManager
|
*PayloadEncryptionManager
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMockEncryptOnlyPayloadManager(aesKey []byte) (*MockEncryptOnlyPayloadManager, error) {
|
func NewMockEncryptOnlyPayloadManager(aesKey []byte) (*MockEncryptOnlyPayloadManager, error) {
|
||||||
|
@ -126,7 +126,7 @@ func NewMockEncryptOnlyPayloadManager(aesKey []byte) (*MockEncryptOnlyPayloadMan
|
||||||
}
|
}
|
||||||
|
|
||||||
return &MockEncryptOnlyPayloadManager{
|
return &MockEncryptOnlyPayloadManager{
|
||||||
pem: pem,
|
pem,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -138,21 +138,9 @@ func (m *MockEncryptOnlyPayloadManager) Mount() error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return m.pem.Encrypt(data)
|
return m.Encrypt(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockEncryptOnlyPayloadManager) Receive(data []byte) error {
|
func (m *MockEncryptOnlyPayloadManager) Receive(data []byte) error {
|
||||||
return m.pem.Decrypt(data)
|
return m.Decrypt(data)
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockEncryptOnlyPayloadManager) ToSend() []byte {
|
|
||||||
return m.pem.ToSend()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockEncryptOnlyPayloadManager) Received() []byte {
|
|
||||||
return m.pem.Received()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockEncryptOnlyPayloadManager) ResetPayload() {
|
|
||||||
m.pem.ResetPayload()
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,18 @@
|
||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/rand"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcutil/base58"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
"github.com/status-im/status-go/ipfs"
|
"github.com/status-im/status-go/ipfs"
|
||||||
|
"github.com/status-im/status-go/protocol/common"
|
||||||
"github.com/status-im/status-go/protocol/identity/identicon"
|
"github.com/status-im/status-go/protocol/identity/identicon"
|
||||||
"github.com/status-im/status-go/protocol/images"
|
"github.com/status-im/status-go/protocol/images"
|
||||||
)
|
)
|
||||||
|
@ -21,14 +25,19 @@ const (
|
||||||
ipfsPath = "/ipfs"
|
ipfsPath = "/ipfs"
|
||||||
|
|
||||||
// Handler routes for pairing
|
// Handler routes for pairing
|
||||||
pairingBase = "/pairing"
|
pairingBase = "/pairing"
|
||||||
pairingSend = pairingBase + "/send"
|
pairingSend = pairingBase + "/send"
|
||||||
pairingReceive = pairingBase + "/receive"
|
pairingReceive = pairingBase + "/receive"
|
||||||
|
pairingChallenge = pairingBase + "/challenge"
|
||||||
|
|
||||||
|
// Session names
|
||||||
|
sessionChallenge = "challenge"
|
||||||
|
sessionBlocked = "blocked"
|
||||||
)
|
)
|
||||||
|
|
||||||
type HandlerPatternMap map[string]http.HandlerFunc
|
type HandlerPatternMap map[string]http.HandlerFunc
|
||||||
|
|
||||||
func handleIdenticon(logger *zap.Logger) func(w http.ResponseWriter, r *http.Request) {
|
func handleIdenticon(logger *zap.Logger) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
pks, ok := r.URL.Query()["publicKey"]
|
pks, ok := r.URL.Query()["publicKey"]
|
||||||
if !ok || len(pks) == 0 {
|
if !ok || len(pks) == 0 {
|
||||||
|
@ -52,7 +61,7 @@ func handleIdenticon(logger *zap.Logger) func(w http.ResponseWriter, r *http.Req
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleImage(db *sql.DB, logger *zap.Logger) func(w http.ResponseWriter, r *http.Request) {
|
func handleImage(db *sql.DB, logger *zap.Logger) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
messageIDs, ok := r.URL.Query()["messageId"]
|
messageIDs, ok := r.URL.Query()["messageId"]
|
||||||
if !ok || len(messageIDs) == 0 {
|
if !ok || len(messageIDs) == 0 {
|
||||||
|
@ -85,7 +94,7 @@ func handleImage(db *sql.DB, logger *zap.Logger) func(w http.ResponseWriter, r *
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleAudio(db *sql.DB, logger *zap.Logger) func(w http.ResponseWriter, r *http.Request) {
|
func handleAudio(db *sql.DB, logger *zap.Logger) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
messageIDs, ok := r.URL.Query()["messageId"]
|
messageIDs, ok := r.URL.Query()["messageId"]
|
||||||
if !ok || len(messageIDs) == 0 {
|
if !ok || len(messageIDs) == 0 {
|
||||||
|
@ -114,7 +123,7 @@ func handleAudio(db *sql.DB, logger *zap.Logger) func(w http.ResponseWriter, r *
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleIPFS(downloader *ipfs.Downloader, logger *zap.Logger) func(w http.ResponseWriter, r *http.Request) {
|
func handleIPFS(downloader *ipfs.Downloader, logger *zap.Logger) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
hashes, ok := r.URL.Query()["hash"]
|
hashes, ok := r.URL.Query()["hash"]
|
||||||
if !ok || len(hashes) == 0 {
|
if !ok || len(hashes) == 0 {
|
||||||
|
@ -140,7 +149,7 @@ func handleIPFS(downloader *ipfs.Downloader, logger *zap.Logger) func(w http.Res
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func handlePairingReceive(ps *PairingServer) func(w http.ResponseWriter, r *http.Request) {
|
func handlePairingReceive(ps *PairingServer) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
payload, err := ioutil.ReadAll(r.Body)
|
payload, err := ioutil.ReadAll(r.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -154,7 +163,8 @@ func handlePairingReceive(ps *PairingServer) func(w http.ResponseWriter, r *http
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func handlePairingSend(ps *PairingServer) func(w http.ResponseWriter, r *http.Request) {
|
func handlePairingSend(ps *PairingServer) http.HandlerFunc {
|
||||||
|
// TODO lock sending after one successful transfer
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/octet-stream")
|
w.Header().Set("Content-Type", "application/octet-stream")
|
||||||
_, err := w.Write(ps.PayloadManager.ToSend())
|
_, err := w.Write(ps.PayloadManager.ToSend())
|
||||||
|
@ -163,3 +173,86 @@ func handlePairingSend(ps *PairingServer) func(w http.ResponseWriter, r *http.Re
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func challengeMiddleware(ps *PairingServer, next http.Handler) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
s, err := ps.cookieStore.Get(r, sessionChallenge)
|
||||||
|
if err != nil {
|
||||||
|
ps.logger.Error("ps.cookieStore.Get(r, pairingStoreChallenge)", zap.Error(err))
|
||||||
|
http.Error(w, "error", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
|
||||||
|
blocked, ok := s.Values[sessionBlocked].(bool)
|
||||||
|
if ok && blocked {
|
||||||
|
http.Error(w, "forbidden", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the request header doesn't include a challenge don't punish the client, just throw a 403
|
||||||
|
pc := r.Header.Get(sessionChallenge)
|
||||||
|
if pc == "" {
|
||||||
|
http.Error(w, "forbidden", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err := common.Decrypt(base58.Decode(pc), ps.ek)
|
||||||
|
if err != nil {
|
||||||
|
ps.logger.Error("c, err := common.Decrypt(rc, ps.ek)", zap.Error(err))
|
||||||
|
http.Error(w, "error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the challenge is not in the session store don't punish the client, just throw a 403
|
||||||
|
challenge, ok := s.Values[sessionChallenge].([]byte)
|
||||||
|
if !ok {
|
||||||
|
http.Error(w, "forbidden", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only if we have both a challenge in the session store and in the request header
|
||||||
|
// do we entertain blocking the client. Because then we know someone is trying to be sneaky.
|
||||||
|
if bytes.Compare(c, challenge) != 0 {
|
||||||
|
s.Values[sessionBlocked] = true
|
||||||
|
err = s.Save(r, w)
|
||||||
|
if err != nil {
|
||||||
|
ps.logger.Error("err = s.Save(r, w)", zap.Error(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
http.Error(w, "forbidden", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handlePairingChallenge(ps *PairingServer) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
s, err := ps.cookieStore.Get(r, sessionChallenge)
|
||||||
|
if err != nil {
|
||||||
|
ps.logger.Error("ps.cookieStore.Get(r, pairingStoreChallenge)", zap.Error(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
var challenge []byte
|
||||||
|
challenge, ok := s.Values[sessionChallenge].([]byte)
|
||||||
|
if !ok {
|
||||||
|
challenge = make([]byte, 64)
|
||||||
|
_, err = rand.Read(challenge)
|
||||||
|
if err != nil {
|
||||||
|
ps.logger.Error("_, err = rand.Read(auth)", zap.Error(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
s.Values[sessionChallenge] = challenge
|
||||||
|
err = s.Save(r, w)
|
||||||
|
if err != nil {
|
||||||
|
ps.logger.Error("err = s.Save(r, w)", zap.Error(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/octet-stream")
|
||||||
|
_, err = w.Write(challenge)
|
||||||
|
if err != nil {
|
||||||
|
ps.logger.Error("_, err = w.Write(challenge)", zap.Error(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -24,6 +24,7 @@ type PayloadManager interface {
|
||||||
ToSend() []byte
|
ToSend() []byte
|
||||||
Received() []byte
|
Received() []byte
|
||||||
ResetPayload()
|
ResetPayload()
|
||||||
|
EncryptPlain(plaintext []byte) ([]byte, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PairingPayloadManagerConfig represents the initialisation parameters required for a PairingPayloadManager
|
// PairingPayloadManagerConfig represents the initialisation parameters required for a PairingPayloadManager
|
||||||
|
@ -36,8 +37,8 @@ type PairingPayloadManagerConfig struct {
|
||||||
|
|
||||||
// PairingPayloadManager is responsible for the whole lifecycle of a PairingPayload
|
// PairingPayloadManager is responsible for the whole lifecycle of a PairingPayload
|
||||||
type PairingPayloadManager struct {
|
type PairingPayloadManager struct {
|
||||||
pp *PairingPayload
|
pp *PairingPayload
|
||||||
pem *PayloadEncryptionManager
|
*PayloadEncryptionManager
|
||||||
ppm *PairingPayloadMarshaller
|
ppm *PairingPayloadMarshaller
|
||||||
ppr PayloadRepository
|
ppr PayloadRepository
|
||||||
}
|
}
|
||||||
|
@ -53,10 +54,10 @@ func NewPairingPayloadManager(aesKey []byte, config *PairingPayloadManagerConfig
|
||||||
p := new(PairingPayload)
|
p := new(PairingPayload)
|
||||||
|
|
||||||
return &PairingPayloadManager{
|
return &PairingPayloadManager{
|
||||||
pp: p,
|
pp: p,
|
||||||
pem: pem,
|
PayloadEncryptionManager: pem,
|
||||||
ppm: NewPairingPayloadMarshaller(p),
|
ppm: NewPairingPayloadMarshaller(p),
|
||||||
ppr: NewPairingPayloadRepository(p, config),
|
ppr: NewPairingPayloadRepository(p, config),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,17 +73,17 @@ func (ppm *PairingPayloadManager) Mount() error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return ppm.pem.Encrypt(pb)
|
return ppm.Encrypt(pb)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Receive takes a []byte representing raw data, parses and stores the data
|
// Receive takes a []byte representing raw data, parses and stores the data
|
||||||
func (ppm *PairingPayloadManager) Receive(data []byte) error {
|
func (ppm *PairingPayloadManager) Receive(data []byte) error {
|
||||||
err := ppm.pem.Decrypt(data)
|
err := ppm.Decrypt(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = ppm.ppm.UnmarshalProtobuf(ppm.pem.Received())
|
err = ppm.ppm.UnmarshalProtobuf(ppm.Received())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -90,20 +91,10 @@ func (ppm *PairingPayloadManager) Receive(data []byte) error {
|
||||||
return ppm.ppr.StoreToSource()
|
return ppm.ppr.StoreToSource()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToSend returns the result of Mount
|
|
||||||
func (ppm *PairingPayloadManager) ToSend() []byte {
|
|
||||||
return ppm.pem.ToSend()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Received returns the decrypted input of Receive
|
|
||||||
func (ppm *PairingPayloadManager) Received() []byte {
|
|
||||||
return ppm.pem.Received()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ResetPayload resets all payload state managed by the PairingPayloadManager
|
// ResetPayload resets all payload state managed by the PairingPayloadManager
|
||||||
func (ppm *PairingPayloadManager) ResetPayload() {
|
func (ppm *PairingPayloadManager) ResetPayload() {
|
||||||
ppm.pp.ResetPayload()
|
ppm.pp.ResetPayload()
|
||||||
ppm.pem.ResetPayload()
|
ppm.PayloadEncryptionManager.ResetPayload()
|
||||||
}
|
}
|
||||||
|
|
||||||
// EncryptionPayload represents the plain text and encrypted text of payload data
|
// EncryptionPayload represents the plain text and encrypted text of payload data
|
||||||
|
@ -123,6 +114,10 @@ func NewPayloadEncryptionManager(aesKey []byte) (*PayloadEncryptionManager, erro
|
||||||
return &PayloadEncryptionManager{aesKey, new(EncryptionPayload), new(EncryptionPayload)}, nil
|
return &PayloadEncryptionManager{aesKey, new(EncryptionPayload), new(EncryptionPayload)}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (pem *PayloadEncryptionManager) EncryptPlain(plaintext []byte) ([]byte, error) {
|
||||||
|
return common.Encrypt(plaintext, pem.aesKey, rand.Reader)
|
||||||
|
}
|
||||||
|
|
||||||
func (pem *PayloadEncryptionManager) Encrypt(data []byte) error {
|
func (pem *PayloadEncryptionManager) Encrypt(data []byte) error {
|
||||||
ep, err := common.Encrypt(data, pem.aesKey, rand.Reader)
|
ep, err := common.Encrypt(data, pem.aesKey, rand.Reader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -2,9 +2,12 @@ package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/ecdsa"
|
"crypto/ecdsa"
|
||||||
|
"crypto/rand"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
|
||||||
|
"github.com/gorilla/sessions"
|
||||||
)
|
)
|
||||||
|
|
||||||
type PairingServer struct {
|
type PairingServer struct {
|
||||||
|
@ -14,6 +17,8 @@ type PairingServer struct {
|
||||||
pk *ecdsa.PublicKey
|
pk *ecdsa.PublicKey
|
||||||
ek []byte
|
ek []byte
|
||||||
mode Mode
|
mode Mode
|
||||||
|
|
||||||
|
cookieStore *sessions.CookieStore
|
||||||
}
|
}
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
|
@ -28,6 +33,22 @@ type Config struct {
|
||||||
*PairingPayloadManagerConfig
|
*PairingPayloadManagerConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func makeCookieStore() (*sessions.CookieStore, error) {
|
||||||
|
auth := make([]byte, 64)
|
||||||
|
_, err := rand.Read(auth)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
enc := make([]byte, 32)
|
||||||
|
_, err = rand.Read(enc)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return sessions.NewCookieStore(auth, enc), nil
|
||||||
|
}
|
||||||
|
|
||||||
// NewPairingServer returns a *PairingServer init from the given *Config
|
// NewPairingServer returns a *PairingServer init from the given *Config
|
||||||
func NewPairingServer(config *Config) (*PairingServer, error) {
|
func NewPairingServer(config *Config) (*PairingServer, error) {
|
||||||
pm, err := NewPairingPayloadManager(config.EK, config.PairingPayloadManagerConfig)
|
pm, err := NewPairingPayloadManager(config.EK, config.PairingPayloadManagerConfig)
|
||||||
|
@ -35,6 +56,11 @@ func NewPairingServer(config *Config) (*PairingServer, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cs, err := makeCookieStore()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
return &PairingServer{Server: NewServer(
|
return &PairingServer{Server: NewServer(
|
||||||
config.Cert,
|
config.Cert,
|
||||||
config.Hostname,
|
config.Hostname,
|
||||||
|
@ -42,7 +68,9 @@ func NewPairingServer(config *Config) (*PairingServer, error) {
|
||||||
pk: config.PK,
|
pk: config.PK,
|
||||||
ek: config.EK,
|
ek: config.EK,
|
||||||
mode: config.Mode,
|
mode: config.Mode,
|
||||||
PayloadManager: pm}, nil
|
PayloadManager: pm,
|
||||||
|
cookieStore: cs,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// MakeConnectionParams generates a *ConnectionParams based on the Server's current state
|
// MakeConnectionParams generates a *ConnectionParams based on the Server's current state
|
||||||
|
@ -76,11 +104,17 @@ func (s *PairingServer) StartPairing() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *PairingServer) startReceivingAccountData() error {
|
func (s *PairingServer) startReceivingAccountData() error {
|
||||||
s.SetHandlers(HandlerPatternMap{pairingReceive: handlePairingReceive(s)})
|
s.SetHandlers(HandlerPatternMap{
|
||||||
|
pairingReceive: handlePairingReceive(s),
|
||||||
|
pairingChallenge: handlePairingChallenge(s),
|
||||||
|
})
|
||||||
return s.Start()
|
return s.Start()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *PairingServer) startSendingAccountData() error {
|
func (s *PairingServer) startSendingAccountData() error {
|
||||||
s.SetHandlers(HandlerPatternMap{pairingSend: handlePairingSend(s)})
|
s.SetHandlers(HandlerPatternMap{
|
||||||
|
pairingSend: challengeMiddleware(s, handlePairingSend(s)),
|
||||||
|
pairingChallenge: handlePairingChallenge(s),
|
||||||
|
})
|
||||||
return s.Start()
|
return s.Start()
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/ecdsa"
|
"crypto/ecdsa"
|
||||||
|
"crypto/rand"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -85,13 +86,13 @@ func (s *PairingServerSuite) TestPairingServer_StartPairing() {
|
||||||
|
|
||||||
switch m {
|
switch m {
|
||||||
case Receiving:
|
case Receiving:
|
||||||
s.Require().Equal(c.PayloadManager.(*MockEncryptOnlyPayloadManager).pem.toSend.plain, s.PS.Received())
|
s.Require().Equal(c.PayloadManager.(*MockEncryptOnlyPayloadManager).toSend.plain, s.PS.Received())
|
||||||
s.Require().Equal(s.PS.PayloadManager.(*MockEncryptOnlyPayloadManager).pem.received.encrypted, c.PayloadManager.(*MockEncryptOnlyPayloadManager).pem.toSend.encrypted)
|
s.Require().Equal(s.PS.PayloadManager.(*MockEncryptOnlyPayloadManager).received.encrypted, c.PayloadManager.(*MockEncryptOnlyPayloadManager).toSend.encrypted)
|
||||||
s.Require().Nil(s.PS.ToSend())
|
s.Require().Nil(s.PS.ToSend())
|
||||||
s.Require().Nil(c.Received())
|
s.Require().Nil(c.Received())
|
||||||
case Sending:
|
case Sending:
|
||||||
s.Require().Equal(c.Received(), s.PS.PayloadManager.(*MockEncryptOnlyPayloadManager).pem.toSend.plain)
|
s.Require().Equal(c.Received(), s.PS.PayloadManager.(*MockEncryptOnlyPayloadManager).toSend.plain)
|
||||||
s.Require().Equal(c.PayloadManager.(*MockEncryptOnlyPayloadManager).pem.received.encrypted, s.PS.PayloadManager.(*MockEncryptOnlyPayloadManager).pem.toSend.encrypted)
|
s.Require().Equal(c.PayloadManager.(*MockEncryptOnlyPayloadManager).received.encrypted, s.PS.PayloadManager.(*MockEncryptOnlyPayloadManager).toSend.encrypted)
|
||||||
s.Require().Nil(c.ToSend())
|
s.Require().Nil(c.ToSend())
|
||||||
s.Require().Nil(s.PS.Received())
|
s.Require().Nil(s.PS.Received())
|
||||||
}
|
}
|
||||||
|
@ -100,3 +101,95 @@ func (s *PairingServerSuite) TestPairingServer_StartPairing() {
|
||||||
s.PS.PayloadManager.(*MockEncryptOnlyPayloadManager).ResetPayload()
|
s.PS.PayloadManager.(*MockEncryptOnlyPayloadManager).ResetPayload()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *PairingServerSuite) sendingSetup() *PairingClient {
|
||||||
|
// Replace PairingServer.PayloadManager with a MockEncryptOnlyPayloadManager
|
||||||
|
pm, err := NewMockEncryptOnlyPayloadManager(s.EphemeralAES)
|
||||||
|
s.Require().NoError(err)
|
||||||
|
s.PS.PayloadManager = pm
|
||||||
|
s.PS.mode = Sending
|
||||||
|
|
||||||
|
err = s.PS.Mount()
|
||||||
|
s.Require().NoError(err)
|
||||||
|
|
||||||
|
err = s.PS.StartPairing()
|
||||||
|
s.Require().NoError(err)
|
||||||
|
|
||||||
|
// Give time for the sever to be ready, hacky I know, I'll iron this out
|
||||||
|
time.Sleep(10 * time.Millisecond)
|
||||||
|
|
||||||
|
cp, err := s.PS.MakeConnectionParams()
|
||||||
|
s.Require().NoError(err)
|
||||||
|
|
||||||
|
qr := cp.ToString()
|
||||||
|
|
||||||
|
// Client reads QR code and parses the connection string
|
||||||
|
ccp := new(ConnectionParams)
|
||||||
|
err = ccp.FromString(qr)
|
||||||
|
s.Require().NoError(err)
|
||||||
|
|
||||||
|
c, err := NewPairingClient(ccp, nil)
|
||||||
|
s.Require().NoError(err)
|
||||||
|
|
||||||
|
// Replace PairingClient.PayloadManager with a MockEncryptOnlyPayloadManager
|
||||||
|
c.PayloadManager, err = NewMockEncryptOnlyPayloadManager(s.EphemeralAES)
|
||||||
|
s.Require().NoError(err)
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PairingServerSuite) TestPairingServer_handlePairingChallengeMiddleware() {
|
||||||
|
c := s.sendingSetup()
|
||||||
|
|
||||||
|
// Attempt to get the private key data, this should fail because there is no challenge
|
||||||
|
err := c.receiveAccountData()
|
||||||
|
s.Require().Error(err)
|
||||||
|
s.Require().Equal("status not ok, received '403 Forbidden'", err.Error())
|
||||||
|
|
||||||
|
err = c.getChallenge()
|
||||||
|
s.Require().NoError(err)
|
||||||
|
challenge := c.serverChallenge
|
||||||
|
|
||||||
|
// This is NOT a mistake! Call c.getChallenge() twice to check that the client gets the same challenge
|
||||||
|
// the server will only generate 1 challenge per session per connection
|
||||||
|
err = c.getChallenge()
|
||||||
|
s.Require().NoError(err)
|
||||||
|
s.Require().Equal(challenge, c.serverChallenge)
|
||||||
|
|
||||||
|
// receiving account data should now work.
|
||||||
|
err = c.receiveAccountData()
|
||||||
|
s.Require().NoError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PairingServerSuite) TestPairingServer_handlePairingChallengeMiddleware_block() {
|
||||||
|
c := s.sendingSetup()
|
||||||
|
|
||||||
|
// Attempt to get the private key data, this should fail because there is no challenge
|
||||||
|
err := c.receiveAccountData()
|
||||||
|
s.Require().Error(err)
|
||||||
|
s.Require().Equal("status not ok, received '403 Forbidden'", err.Error())
|
||||||
|
|
||||||
|
// Get the challenge
|
||||||
|
err = c.getChallenge()
|
||||||
|
s.Require().NoError(err)
|
||||||
|
|
||||||
|
// Simulate encrypting with a dodgy key, write some nonsense to the challenge field
|
||||||
|
c.serverChallenge = make([]byte, 64)
|
||||||
|
_, err = rand.Read(c.serverChallenge)
|
||||||
|
s.Require().NoError(err)
|
||||||
|
|
||||||
|
// Attempt again to get the account data, should fail
|
||||||
|
// behind the scenes the server will block the session if the client fails the challenge. There is no forgiveness!
|
||||||
|
err = c.receiveAccountData()
|
||||||
|
s.Require().Error(err)
|
||||||
|
s.Require().Equal("status not ok, received '403 Forbidden'", err.Error())
|
||||||
|
|
||||||
|
// Get the real challenge
|
||||||
|
err = c.getChallenge()
|
||||||
|
s.Require().NoError(err)
|
||||||
|
|
||||||
|
// Attempt to get the account data, should fail because the client is now blocked.
|
||||||
|
err = c.receiveAccountData()
|
||||||
|
s.Require().Error(err)
|
||||||
|
s.Require().Equal("status not ok, received '403 Forbidden'", err.Error())
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
language: go
|
||||||
|
sudo: false
|
||||||
|
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- go: 1.3
|
||||||
|
- go: 1.4
|
||||||
|
- go: 1.5
|
||||||
|
- go: 1.6
|
||||||
|
- go: 1.7
|
||||||
|
- go: tip
|
||||||
|
allow_failures:
|
||||||
|
- go: tip
|
||||||
|
|
||||||
|
script:
|
||||||
|
- go get -t -v ./...
|
||||||
|
- diff -u <(echo -n) <(gofmt -d .)
|
||||||
|
- go vet $(go list ./... | grep -v /vendor/)
|
||||||
|
- go test -v -race ./...
|
|
@ -0,0 +1,27 @@
|
||||||
|
Copyright (c) 2012 Rodrigo Moraes. All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are
|
||||||
|
met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
* Redistributions in binary form must reproduce the above
|
||||||
|
copyright notice, this list of conditions and the following disclaimer
|
||||||
|
in the documentation and/or other materials provided with the
|
||||||
|
distribution.
|
||||||
|
* Neither the name of Google Inc. nor the names of its
|
||||||
|
contributors may be used to endorse or promote products derived from
|
||||||
|
this software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||||
|
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
|
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||||
|
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||||
|
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@ -0,0 +1,80 @@
|
||||||
|
securecookie
|
||||||
|
============
|
||||||
|
[![GoDoc](https://godoc.org/github.com/gorilla/securecookie?status.svg)](https://godoc.org/github.com/gorilla/securecookie) [![Build Status](https://travis-ci.org/gorilla/securecookie.png?branch=master)](https://travis-ci.org/gorilla/securecookie)
|
||||||
|
[![Sourcegraph](https://sourcegraph.com/github.com/gorilla/securecookie/-/badge.svg)](https://sourcegraph.com/github.com/gorilla/securecookie?badge)
|
||||||
|
|
||||||
|
|
||||||
|
securecookie encodes and decodes authenticated and optionally encrypted
|
||||||
|
cookie values.
|
||||||
|
|
||||||
|
Secure cookies can't be forged, because their values are validated using HMAC.
|
||||||
|
When encrypted, the content is also inaccessible to malicious eyes. It is still
|
||||||
|
recommended that sensitive data not be stored in cookies, and that HTTPS be used
|
||||||
|
to prevent cookie [replay attacks](https://en.wikipedia.org/wiki/Replay_attack).
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
To use it, first create a new SecureCookie instance:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Hash keys should be at least 32 bytes long
|
||||||
|
var hashKey = []byte("very-secret")
|
||||||
|
// Block keys should be 16 bytes (AES-128) or 32 bytes (AES-256) long.
|
||||||
|
// Shorter keys may weaken the encryption used.
|
||||||
|
var blockKey = []byte("a-lot-secret")
|
||||||
|
var s = securecookie.New(hashKey, blockKey)
|
||||||
|
```
|
||||||
|
|
||||||
|
The hashKey is required, used to authenticate the cookie value using HMAC.
|
||||||
|
It is recommended to use a key with 32 or 64 bytes.
|
||||||
|
|
||||||
|
The blockKey is optional, used to encrypt the cookie value -- set it to nil
|
||||||
|
to not use encryption. If set, the length must correspond to the block size
|
||||||
|
of the encryption algorithm. For AES, used by default, valid lengths are
|
||||||
|
16, 24, or 32 bytes to select AES-128, AES-192, or AES-256.
|
||||||
|
|
||||||
|
Strong keys can be created using the convenience function GenerateRandomKey().
|
||||||
|
|
||||||
|
Once a SecureCookie instance is set, use it to encode a cookie value:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func SetCookieHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
value := map[string]string{
|
||||||
|
"foo": "bar",
|
||||||
|
}
|
||||||
|
if encoded, err := s.Encode("cookie-name", value); err == nil {
|
||||||
|
cookie := &http.Cookie{
|
||||||
|
Name: "cookie-name",
|
||||||
|
Value: encoded,
|
||||||
|
Path: "/",
|
||||||
|
Secure: true,
|
||||||
|
HttpOnly: true,
|
||||||
|
}
|
||||||
|
http.SetCookie(w, cookie)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Later, use the same SecureCookie instance to decode and validate a cookie
|
||||||
|
value:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func ReadCookieHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if cookie, err := r.Cookie("cookie-name"); err == nil {
|
||||||
|
value := make(map[string]string)
|
||||||
|
if err = s2.Decode("cookie-name", cookie.Value, &value); err == nil {
|
||||||
|
fmt.Fprintf(w, "The value of foo is %q", value["foo"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
We stored a map[string]string, but secure cookies can hold any value that
|
||||||
|
can be encoded using `encoding/gob`. To store custom types, they must be
|
||||||
|
registered first using gob.Register(). For basic types this is not needed;
|
||||||
|
it works out of the box. An optional JSON encoder that uses `encoding/json` is
|
||||||
|
available for types compatible with JSON.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
BSD licensed. See the LICENSE file for details.
|
|
@ -0,0 +1,61 @@
|
||||||
|
// Copyright 2012 The Gorilla Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
/*
|
||||||
|
Package securecookie encodes and decodes authenticated and optionally
|
||||||
|
encrypted cookie values.
|
||||||
|
|
||||||
|
Secure cookies can't be forged, because their values are validated using HMAC.
|
||||||
|
When encrypted, the content is also inaccessible to malicious eyes.
|
||||||
|
|
||||||
|
To use it, first create a new SecureCookie instance:
|
||||||
|
|
||||||
|
var hashKey = []byte("very-secret")
|
||||||
|
var blockKey = []byte("a-lot-secret")
|
||||||
|
var s = securecookie.New(hashKey, blockKey)
|
||||||
|
|
||||||
|
The hashKey is required, used to authenticate the cookie value using HMAC.
|
||||||
|
It is recommended to use a key with 32 or 64 bytes.
|
||||||
|
|
||||||
|
The blockKey is optional, used to encrypt the cookie value -- set it to nil
|
||||||
|
to not use encryption. If set, the length must correspond to the block size
|
||||||
|
of the encryption algorithm. For AES, used by default, valid lengths are
|
||||||
|
16, 24, or 32 bytes to select AES-128, AES-192, or AES-256.
|
||||||
|
|
||||||
|
Strong keys can be created using the convenience function GenerateRandomKey().
|
||||||
|
|
||||||
|
Once a SecureCookie instance is set, use it to encode a cookie value:
|
||||||
|
|
||||||
|
func SetCookieHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
value := map[string]string{
|
||||||
|
"foo": "bar",
|
||||||
|
}
|
||||||
|
if encoded, err := s.Encode("cookie-name", value); err == nil {
|
||||||
|
cookie := &http.Cookie{
|
||||||
|
Name: "cookie-name",
|
||||||
|
Value: encoded,
|
||||||
|
Path: "/",
|
||||||
|
}
|
||||||
|
http.SetCookie(w, cookie)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Later, use the same SecureCookie instance to decode and validate a cookie
|
||||||
|
value:
|
||||||
|
|
||||||
|
func ReadCookieHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if cookie, err := r.Cookie("cookie-name"); err == nil {
|
||||||
|
value := make(map[string]string)
|
||||||
|
if err = s2.Decode("cookie-name", cookie.Value, &value); err == nil {
|
||||||
|
fmt.Fprintf(w, "The value of foo is %q", value["foo"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
We stored a map[string]string, but secure cookies can hold any value that
|
||||||
|
can be encoded using encoding/gob. To store custom types, they must be
|
||||||
|
registered first using gob.Register(). For basic types this is not needed;
|
||||||
|
it works out of the box.
|
||||||
|
*/
|
||||||
|
package securecookie
|
|
@ -0,0 +1,25 @@
|
||||||
|
// +build gofuzz
|
||||||
|
|
||||||
|
package securecookie
|
||||||
|
|
||||||
|
var hashKey = []byte("very-secret12345")
|
||||||
|
var blockKey = []byte("a-lot-secret1234")
|
||||||
|
var s = New(hashKey, blockKey)
|
||||||
|
|
||||||
|
type Cookie struct {
|
||||||
|
B bool
|
||||||
|
I int
|
||||||
|
S string
|
||||||
|
}
|
||||||
|
|
||||||
|
func Fuzz(data []byte) int {
|
||||||
|
datas := string(data)
|
||||||
|
var c Cookie
|
||||||
|
if err := s.Decode("fuzz", datas, &c); err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if _, err := s.Encode("fuzz", c); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return 1
|
||||||
|
}
|
|
@ -0,0 +1,646 @@
|
||||||
|
// Copyright 2012 The Gorilla Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package securecookie
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/aes"
|
||||||
|
"crypto/cipher"
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/sha256"
|
||||||
|
"crypto/subtle"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/gob"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"hash"
|
||||||
|
"io"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Error is the interface of all errors returned by functions in this library.
|
||||||
|
type Error interface {
|
||||||
|
error
|
||||||
|
|
||||||
|
// IsUsage returns true for errors indicating the client code probably
|
||||||
|
// uses this library incorrectly. For example, the client may have
|
||||||
|
// failed to provide a valid hash key, or may have failed to configure
|
||||||
|
// the Serializer adequately for encoding value.
|
||||||
|
IsUsage() bool
|
||||||
|
|
||||||
|
// IsDecode returns true for errors indicating that a cookie could not
|
||||||
|
// be decoded and validated. Since cookies are usually untrusted
|
||||||
|
// user-provided input, errors of this type should be expected.
|
||||||
|
// Usually, the proper action is simply to reject the request.
|
||||||
|
IsDecode() bool
|
||||||
|
|
||||||
|
// IsInternal returns true for unexpected errors occurring in the
|
||||||
|
// securecookie implementation.
|
||||||
|
IsInternal() bool
|
||||||
|
|
||||||
|
// Cause, if it returns a non-nil value, indicates that this error was
|
||||||
|
// propagated from some underlying library. If this method returns nil,
|
||||||
|
// this error was raised directly by this library.
|
||||||
|
//
|
||||||
|
// Cause is provided principally for debugging/logging purposes; it is
|
||||||
|
// rare that application logic should perform meaningfully different
|
||||||
|
// logic based on Cause. See, for example, the caveats described on
|
||||||
|
// (MultiError).Cause().
|
||||||
|
Cause() error
|
||||||
|
}
|
||||||
|
|
||||||
|
// errorType is a bitmask giving the error type(s) of an cookieError value.
|
||||||
|
type errorType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
usageError = errorType(1 << iota)
|
||||||
|
decodeError
|
||||||
|
internalError
|
||||||
|
)
|
||||||
|
|
||||||
|
type cookieError struct {
|
||||||
|
typ errorType
|
||||||
|
msg string
|
||||||
|
cause error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e cookieError) IsUsage() bool { return (e.typ & usageError) != 0 }
|
||||||
|
func (e cookieError) IsDecode() bool { return (e.typ & decodeError) != 0 }
|
||||||
|
func (e cookieError) IsInternal() bool { return (e.typ & internalError) != 0 }
|
||||||
|
|
||||||
|
func (e cookieError) Cause() error { return e.cause }
|
||||||
|
|
||||||
|
func (e cookieError) Error() string {
|
||||||
|
parts := []string{"securecookie: "}
|
||||||
|
if e.msg == "" {
|
||||||
|
parts = append(parts, "error")
|
||||||
|
} else {
|
||||||
|
parts = append(parts, e.msg)
|
||||||
|
}
|
||||||
|
if c := e.Cause(); c != nil {
|
||||||
|
parts = append(parts, " - caused by: ", c.Error())
|
||||||
|
}
|
||||||
|
return strings.Join(parts, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
errGeneratingIV = cookieError{typ: internalError, msg: "failed to generate random iv"}
|
||||||
|
|
||||||
|
errNoCodecs = cookieError{typ: usageError, msg: "no codecs provided"}
|
||||||
|
errHashKeyNotSet = cookieError{typ: usageError, msg: "hash key is not set"}
|
||||||
|
errBlockKeyNotSet = cookieError{typ: usageError, msg: "block key is not set"}
|
||||||
|
errEncodedValueTooLong = cookieError{typ: usageError, msg: "the value is too long"}
|
||||||
|
|
||||||
|
errValueToDecodeTooLong = cookieError{typ: decodeError, msg: "the value is too long"}
|
||||||
|
errTimestampInvalid = cookieError{typ: decodeError, msg: "invalid timestamp"}
|
||||||
|
errTimestampTooNew = cookieError{typ: decodeError, msg: "timestamp is too new"}
|
||||||
|
errTimestampExpired = cookieError{typ: decodeError, msg: "expired timestamp"}
|
||||||
|
errDecryptionFailed = cookieError{typ: decodeError, msg: "the value could not be decrypted"}
|
||||||
|
errValueNotByte = cookieError{typ: decodeError, msg: "value not a []byte."}
|
||||||
|
errValueNotBytePtr = cookieError{typ: decodeError, msg: "value not a pointer to []byte."}
|
||||||
|
|
||||||
|
// ErrMacInvalid indicates that cookie decoding failed because the HMAC
|
||||||
|
// could not be extracted and verified. Direct use of this error
|
||||||
|
// variable is deprecated; it is public only for legacy compatibility,
|
||||||
|
// and may be privatized in the future, as it is rarely useful to
|
||||||
|
// distinguish between this error and other Error implementations.
|
||||||
|
ErrMacInvalid = cookieError{typ: decodeError, msg: "the value is not valid"}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Codec defines an interface to encode and decode cookie values.
|
||||||
|
type Codec interface {
|
||||||
|
Encode(name string, value interface{}) (string, error)
|
||||||
|
Decode(name, value string, dst interface{}) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// New returns a new SecureCookie.
|
||||||
|
//
|
||||||
|
// hashKey is required, used to authenticate values using HMAC. Create it using
|
||||||
|
// GenerateRandomKey(). It is recommended to use a key with 32 or 64 bytes.
|
||||||
|
//
|
||||||
|
// blockKey is optional, used to encrypt values. Create it using
|
||||||
|
// GenerateRandomKey(). The key length must correspond to the block size
|
||||||
|
// of the encryption algorithm. For AES, used by default, valid lengths are
|
||||||
|
// 16, 24, or 32 bytes to select AES-128, AES-192, or AES-256.
|
||||||
|
// The default encoder used for cookie serialization is encoding/gob.
|
||||||
|
//
|
||||||
|
// Note that keys created using GenerateRandomKey() are not automatically
|
||||||
|
// persisted. New keys will be created when the application is restarted, and
|
||||||
|
// previously issued cookies will not be able to be decoded.
|
||||||
|
func New(hashKey, blockKey []byte) *SecureCookie {
|
||||||
|
s := &SecureCookie{
|
||||||
|
hashKey: hashKey,
|
||||||
|
blockKey: blockKey,
|
||||||
|
hashFunc: sha256.New,
|
||||||
|
maxAge: 86400 * 30,
|
||||||
|
maxLength: 4096,
|
||||||
|
sz: GobEncoder{},
|
||||||
|
}
|
||||||
|
if hashKey == nil {
|
||||||
|
s.err = errHashKeyNotSet
|
||||||
|
}
|
||||||
|
if blockKey != nil {
|
||||||
|
s.BlockFunc(aes.NewCipher)
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// SecureCookie encodes and decodes authenticated and optionally encrypted
|
||||||
|
// cookie values.
|
||||||
|
type SecureCookie struct {
|
||||||
|
hashKey []byte
|
||||||
|
hashFunc func() hash.Hash
|
||||||
|
blockKey []byte
|
||||||
|
block cipher.Block
|
||||||
|
maxLength int
|
||||||
|
maxAge int64
|
||||||
|
minAge int64
|
||||||
|
err error
|
||||||
|
sz Serializer
|
||||||
|
// For testing purposes, the function that returns the current timestamp.
|
||||||
|
// If not set, it will use time.Now().UTC().Unix().
|
||||||
|
timeFunc func() int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serializer provides an interface for providing custom serializers for cookie
|
||||||
|
// values.
|
||||||
|
type Serializer interface {
|
||||||
|
Serialize(src interface{}) ([]byte, error)
|
||||||
|
Deserialize(src []byte, dst interface{}) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// GobEncoder encodes cookie values using encoding/gob. This is the simplest
|
||||||
|
// encoder and can handle complex types via gob.Register.
|
||||||
|
type GobEncoder struct{}
|
||||||
|
|
||||||
|
// JSONEncoder encodes cookie values using encoding/json. Users who wish to
|
||||||
|
// encode complex types need to satisfy the json.Marshaller and
|
||||||
|
// json.Unmarshaller interfaces.
|
||||||
|
type JSONEncoder struct{}
|
||||||
|
|
||||||
|
// NopEncoder does not encode cookie values, and instead simply accepts a []byte
|
||||||
|
// (as an interface{}) and returns a []byte. This is particularly useful when
|
||||||
|
// you encoding an object upstream and do not wish to re-encode it.
|
||||||
|
type NopEncoder struct{}
|
||||||
|
|
||||||
|
// MaxLength restricts the maximum length, in bytes, for the cookie value.
|
||||||
|
//
|
||||||
|
// Default is 4096, which is the maximum value accepted by Internet Explorer.
|
||||||
|
func (s *SecureCookie) MaxLength(value int) *SecureCookie {
|
||||||
|
s.maxLength = value
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// MaxAge restricts the maximum age, in seconds, for the cookie value.
|
||||||
|
//
|
||||||
|
// Default is 86400 * 30. Set it to 0 for no restriction.
|
||||||
|
func (s *SecureCookie) MaxAge(value int) *SecureCookie {
|
||||||
|
s.maxAge = int64(value)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// MinAge restricts the minimum age, in seconds, for the cookie value.
|
||||||
|
//
|
||||||
|
// Default is 0 (no restriction).
|
||||||
|
func (s *SecureCookie) MinAge(value int) *SecureCookie {
|
||||||
|
s.minAge = int64(value)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// HashFunc sets the hash function used to create HMAC.
|
||||||
|
//
|
||||||
|
// Default is crypto/sha256.New.
|
||||||
|
func (s *SecureCookie) HashFunc(f func() hash.Hash) *SecureCookie {
|
||||||
|
s.hashFunc = f
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// BlockFunc sets the encryption function used to create a cipher.Block.
|
||||||
|
//
|
||||||
|
// Default is crypto/aes.New.
|
||||||
|
func (s *SecureCookie) BlockFunc(f func([]byte) (cipher.Block, error)) *SecureCookie {
|
||||||
|
if s.blockKey == nil {
|
||||||
|
s.err = errBlockKeyNotSet
|
||||||
|
} else if block, err := f(s.blockKey); err == nil {
|
||||||
|
s.block = block
|
||||||
|
} else {
|
||||||
|
s.err = cookieError{cause: err, typ: usageError}
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encoding sets the encoding/serialization method for cookies.
|
||||||
|
//
|
||||||
|
// Default is encoding/gob. To encode special structures using encoding/gob,
|
||||||
|
// they must be registered first using gob.Register().
|
||||||
|
func (s *SecureCookie) SetSerializer(sz Serializer) *SecureCookie {
|
||||||
|
s.sz = sz
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode encodes a cookie value.
|
||||||
|
//
|
||||||
|
// It serializes, optionally encrypts, signs with a message authentication code,
|
||||||
|
// and finally encodes the value.
|
||||||
|
//
|
||||||
|
// The name argument is the cookie name. It is stored with the encoded value.
|
||||||
|
// The value argument is the value to be encoded. It can be any value that can
|
||||||
|
// be encoded using the currently selected serializer; see SetSerializer().
|
||||||
|
//
|
||||||
|
// It is the client's responsibility to ensure that value, when encoded using
|
||||||
|
// the current serialization/encryption settings on s and then base64-encoded,
|
||||||
|
// is shorter than the maximum permissible length.
|
||||||
|
func (s *SecureCookie) Encode(name string, value interface{}) (string, error) {
|
||||||
|
if s.err != nil {
|
||||||
|
return "", s.err
|
||||||
|
}
|
||||||
|
if s.hashKey == nil {
|
||||||
|
s.err = errHashKeyNotSet
|
||||||
|
return "", s.err
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
var b []byte
|
||||||
|
// 1. Serialize.
|
||||||
|
if b, err = s.sz.Serialize(value); err != nil {
|
||||||
|
return "", cookieError{cause: err, typ: usageError}
|
||||||
|
}
|
||||||
|
// 2. Encrypt (optional).
|
||||||
|
if s.block != nil {
|
||||||
|
if b, err = encrypt(s.block, b); err != nil {
|
||||||
|
return "", cookieError{cause: err, typ: usageError}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b = encode(b)
|
||||||
|
// 3. Create MAC for "name|date|value". Extra pipe to be used later.
|
||||||
|
b = []byte(fmt.Sprintf("%s|%d|%s|", name, s.timestamp(), b))
|
||||||
|
mac := createMac(hmac.New(s.hashFunc, s.hashKey), b[:len(b)-1])
|
||||||
|
// Append mac, remove name.
|
||||||
|
b = append(b, mac...)[len(name)+1:]
|
||||||
|
// 4. Encode to base64.
|
||||||
|
b = encode(b)
|
||||||
|
// 5. Check length.
|
||||||
|
if s.maxLength != 0 && len(b) > s.maxLength {
|
||||||
|
return "", errEncodedValueTooLong
|
||||||
|
}
|
||||||
|
// Done.
|
||||||
|
return string(b), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode decodes a cookie value.
|
||||||
|
//
|
||||||
|
// It decodes, verifies a message authentication code, optionally decrypts and
|
||||||
|
// finally deserializes the value.
|
||||||
|
//
|
||||||
|
// The name argument is the cookie name. It must be the same name used when
|
||||||
|
// it was stored. The value argument is the encoded cookie value. The dst
|
||||||
|
// argument is where the cookie will be decoded. It must be a pointer.
|
||||||
|
func (s *SecureCookie) Decode(name, value string, dst interface{}) error {
|
||||||
|
if s.err != nil {
|
||||||
|
return s.err
|
||||||
|
}
|
||||||
|
if s.hashKey == nil {
|
||||||
|
s.err = errHashKeyNotSet
|
||||||
|
return s.err
|
||||||
|
}
|
||||||
|
// 1. Check length.
|
||||||
|
if s.maxLength != 0 && len(value) > s.maxLength {
|
||||||
|
return errValueToDecodeTooLong
|
||||||
|
}
|
||||||
|
// 2. Decode from base64.
|
||||||
|
b, err := decode([]byte(value))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// 3. Verify MAC. Value is "date|value|mac".
|
||||||
|
parts := bytes.SplitN(b, []byte("|"), 3)
|
||||||
|
if len(parts) != 3 {
|
||||||
|
return ErrMacInvalid
|
||||||
|
}
|
||||||
|
h := hmac.New(s.hashFunc, s.hashKey)
|
||||||
|
b = append([]byte(name+"|"), b[:len(b)-len(parts[2])-1]...)
|
||||||
|
if err = verifyMac(h, b, parts[2]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// 4. Verify date ranges.
|
||||||
|
var t1 int64
|
||||||
|
if t1, err = strconv.ParseInt(string(parts[0]), 10, 64); err != nil {
|
||||||
|
return errTimestampInvalid
|
||||||
|
}
|
||||||
|
t2 := s.timestamp()
|
||||||
|
if s.minAge != 0 && t1 > t2-s.minAge {
|
||||||
|
return errTimestampTooNew
|
||||||
|
}
|
||||||
|
if s.maxAge != 0 && t1 < t2-s.maxAge {
|
||||||
|
return errTimestampExpired
|
||||||
|
}
|
||||||
|
// 5. Decrypt (optional).
|
||||||
|
b, err = decode(parts[1])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if s.block != nil {
|
||||||
|
if b, err = decrypt(s.block, b); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 6. Deserialize.
|
||||||
|
if err = s.sz.Deserialize(b, dst); err != nil {
|
||||||
|
return cookieError{cause: err, typ: decodeError}
|
||||||
|
}
|
||||||
|
// Done.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// timestamp returns the current timestamp, in seconds.
|
||||||
|
//
|
||||||
|
// For testing purposes, the function that generates the timestamp can be
|
||||||
|
// overridden. If not set, it will return time.Now().UTC().Unix().
|
||||||
|
func (s *SecureCookie) timestamp() int64 {
|
||||||
|
if s.timeFunc == nil {
|
||||||
|
return time.Now().UTC().Unix()
|
||||||
|
}
|
||||||
|
return s.timeFunc()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authentication -------------------------------------------------------------
|
||||||
|
|
||||||
|
// createMac creates a message authentication code (MAC).
|
||||||
|
func createMac(h hash.Hash, value []byte) []byte {
|
||||||
|
h.Write(value)
|
||||||
|
return h.Sum(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// verifyMac verifies that a message authentication code (MAC) is valid.
|
||||||
|
func verifyMac(h hash.Hash, value []byte, mac []byte) error {
|
||||||
|
mac2 := createMac(h, value)
|
||||||
|
// Check that both MACs are of equal length, as subtle.ConstantTimeCompare
|
||||||
|
// does not do this prior to Go 1.4.
|
||||||
|
if len(mac) == len(mac2) && subtle.ConstantTimeCompare(mac, mac2) == 1 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return ErrMacInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encryption -----------------------------------------------------------------
|
||||||
|
|
||||||
|
// encrypt encrypts a value using the given block in counter mode.
|
||||||
|
//
|
||||||
|
// A random initialization vector (http://goo.gl/zF67k) with the length of the
|
||||||
|
// block size is prepended to the resulting ciphertext.
|
||||||
|
func encrypt(block cipher.Block, value []byte) ([]byte, error) {
|
||||||
|
iv := GenerateRandomKey(block.BlockSize())
|
||||||
|
if iv == nil {
|
||||||
|
return nil, errGeneratingIV
|
||||||
|
}
|
||||||
|
// Encrypt it.
|
||||||
|
stream := cipher.NewCTR(block, iv)
|
||||||
|
stream.XORKeyStream(value, value)
|
||||||
|
// Return iv + ciphertext.
|
||||||
|
return append(iv, value...), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// decrypt decrypts a value using the given block in counter mode.
|
||||||
|
//
|
||||||
|
// The value to be decrypted must be prepended by a initialization vector
|
||||||
|
// (http://goo.gl/zF67k) with the length of the block size.
|
||||||
|
func decrypt(block cipher.Block, value []byte) ([]byte, error) {
|
||||||
|
size := block.BlockSize()
|
||||||
|
if len(value) > size {
|
||||||
|
// Extract iv.
|
||||||
|
iv := value[:size]
|
||||||
|
// Extract ciphertext.
|
||||||
|
value = value[size:]
|
||||||
|
// Decrypt it.
|
||||||
|
stream := cipher.NewCTR(block, iv)
|
||||||
|
stream.XORKeyStream(value, value)
|
||||||
|
return value, nil
|
||||||
|
}
|
||||||
|
return nil, errDecryptionFailed
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serialization --------------------------------------------------------------
|
||||||
|
|
||||||
|
// Serialize encodes a value using gob.
|
||||||
|
func (e GobEncoder) Serialize(src interface{}) ([]byte, error) {
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
enc := gob.NewEncoder(buf)
|
||||||
|
if err := enc.Encode(src); err != nil {
|
||||||
|
return nil, cookieError{cause: err, typ: usageError}
|
||||||
|
}
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deserialize decodes a value using gob.
|
||||||
|
func (e GobEncoder) Deserialize(src []byte, dst interface{}) error {
|
||||||
|
dec := gob.NewDecoder(bytes.NewBuffer(src))
|
||||||
|
if err := dec.Decode(dst); err != nil {
|
||||||
|
return cookieError{cause: err, typ: decodeError}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serialize encodes a value using encoding/json.
|
||||||
|
func (e JSONEncoder) Serialize(src interface{}) ([]byte, error) {
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
enc := json.NewEncoder(buf)
|
||||||
|
if err := enc.Encode(src); err != nil {
|
||||||
|
return nil, cookieError{cause: err, typ: usageError}
|
||||||
|
}
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deserialize decodes a value using encoding/json.
|
||||||
|
func (e JSONEncoder) Deserialize(src []byte, dst interface{}) error {
|
||||||
|
dec := json.NewDecoder(bytes.NewReader(src))
|
||||||
|
if err := dec.Decode(dst); err != nil {
|
||||||
|
return cookieError{cause: err, typ: decodeError}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serialize passes a []byte through as-is.
|
||||||
|
func (e NopEncoder) Serialize(src interface{}) ([]byte, error) {
|
||||||
|
if b, ok := src.([]byte); ok {
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errValueNotByte
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deserialize passes a []byte through as-is.
|
||||||
|
func (e NopEncoder) Deserialize(src []byte, dst interface{}) error {
|
||||||
|
if dat, ok := dst.(*[]byte); ok {
|
||||||
|
*dat = src
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return errValueNotBytePtr
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encoding -------------------------------------------------------------------
|
||||||
|
|
||||||
|
// encode encodes a value using base64.
|
||||||
|
func encode(value []byte) []byte {
|
||||||
|
encoded := make([]byte, base64.URLEncoding.EncodedLen(len(value)))
|
||||||
|
base64.URLEncoding.Encode(encoded, value)
|
||||||
|
return encoded
|
||||||
|
}
|
||||||
|
|
||||||
|
// decode decodes a cookie using base64.
|
||||||
|
func decode(value []byte) ([]byte, error) {
|
||||||
|
decoded := make([]byte, base64.URLEncoding.DecodedLen(len(value)))
|
||||||
|
b, err := base64.URLEncoding.Decode(decoded, value)
|
||||||
|
if err != nil {
|
||||||
|
return nil, cookieError{cause: err, typ: decodeError, msg: "base64 decode failed"}
|
||||||
|
}
|
||||||
|
return decoded[:b], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helpers --------------------------------------------------------------------
|
||||||
|
|
||||||
|
// GenerateRandomKey creates a random key with the given length in bytes.
|
||||||
|
// On failure, returns nil.
|
||||||
|
//
|
||||||
|
// Callers should explicitly check for the possibility of a nil return, treat
|
||||||
|
// it as a failure of the system random number generator, and not continue.
|
||||||
|
func GenerateRandomKey(length int) []byte {
|
||||||
|
k := make([]byte, length)
|
||||||
|
if _, err := io.ReadFull(rand.Reader, k); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return k
|
||||||
|
}
|
||||||
|
|
||||||
|
// CodecsFromPairs returns a slice of SecureCookie instances.
|
||||||
|
//
|
||||||
|
// It is a convenience function to create a list of codecs for key rotation. Note
|
||||||
|
// that the generated Codecs will have the default options applied: callers
|
||||||
|
// should iterate over each Codec and type-assert the underlying *SecureCookie to
|
||||||
|
// change these.
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
//
|
||||||
|
// codecs := securecookie.CodecsFromPairs(
|
||||||
|
// []byte("new-hash-key"),
|
||||||
|
// []byte("new-block-key"),
|
||||||
|
// []byte("old-hash-key"),
|
||||||
|
// []byte("old-block-key"),
|
||||||
|
// )
|
||||||
|
//
|
||||||
|
// // Modify each instance.
|
||||||
|
// for _, s := range codecs {
|
||||||
|
// if cookie, ok := s.(*securecookie.SecureCookie); ok {
|
||||||
|
// cookie.MaxAge(86400 * 7)
|
||||||
|
// cookie.SetSerializer(securecookie.JSONEncoder{})
|
||||||
|
// cookie.HashFunc(sha512.New512_256)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
func CodecsFromPairs(keyPairs ...[]byte) []Codec {
|
||||||
|
codecs := make([]Codec, len(keyPairs)/2+len(keyPairs)%2)
|
||||||
|
for i := 0; i < len(keyPairs); i += 2 {
|
||||||
|
var blockKey []byte
|
||||||
|
if i+1 < len(keyPairs) {
|
||||||
|
blockKey = keyPairs[i+1]
|
||||||
|
}
|
||||||
|
codecs[i/2] = New(keyPairs[i], blockKey)
|
||||||
|
}
|
||||||
|
return codecs
|
||||||
|
}
|
||||||
|
|
||||||
|
// EncodeMulti encodes a cookie value using a group of codecs.
|
||||||
|
//
|
||||||
|
// The codecs are tried in order. Multiple codecs are accepted to allow
|
||||||
|
// key rotation.
|
||||||
|
//
|
||||||
|
// On error, may return a MultiError.
|
||||||
|
func EncodeMulti(name string, value interface{}, codecs ...Codec) (string, error) {
|
||||||
|
if len(codecs) == 0 {
|
||||||
|
return "", errNoCodecs
|
||||||
|
}
|
||||||
|
|
||||||
|
var errors MultiError
|
||||||
|
for _, codec := range codecs {
|
||||||
|
encoded, err := codec.Encode(name, value)
|
||||||
|
if err == nil {
|
||||||
|
return encoded, nil
|
||||||
|
}
|
||||||
|
errors = append(errors, err)
|
||||||
|
}
|
||||||
|
return "", errors
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecodeMulti decodes a cookie value using a group of codecs.
|
||||||
|
//
|
||||||
|
// The codecs are tried in order. Multiple codecs are accepted to allow
|
||||||
|
// key rotation.
|
||||||
|
//
|
||||||
|
// On error, may return a MultiError.
|
||||||
|
func DecodeMulti(name string, value string, dst interface{}, codecs ...Codec) error {
|
||||||
|
if len(codecs) == 0 {
|
||||||
|
return errNoCodecs
|
||||||
|
}
|
||||||
|
|
||||||
|
var errors MultiError
|
||||||
|
for _, codec := range codecs {
|
||||||
|
err := codec.Decode(name, value, dst)
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
errors = append(errors, err)
|
||||||
|
}
|
||||||
|
return errors
|
||||||
|
}
|
||||||
|
|
||||||
|
// MultiError groups multiple errors.
|
||||||
|
type MultiError []error
|
||||||
|
|
||||||
|
func (m MultiError) IsUsage() bool { return m.any(func(e Error) bool { return e.IsUsage() }) }
|
||||||
|
func (m MultiError) IsDecode() bool { return m.any(func(e Error) bool { return e.IsDecode() }) }
|
||||||
|
func (m MultiError) IsInternal() bool { return m.any(func(e Error) bool { return e.IsInternal() }) }
|
||||||
|
|
||||||
|
// Cause returns nil for MultiError; there is no unique underlying cause in the
|
||||||
|
// general case.
|
||||||
|
//
|
||||||
|
// Note: we could conceivably return a non-nil Cause only when there is exactly
|
||||||
|
// one child error with a Cause. However, it would be brittle for client code
|
||||||
|
// to rely on the arity of causes inside a MultiError, so we have opted not to
|
||||||
|
// provide this functionality. Clients which really wish to access the Causes
|
||||||
|
// of the underlying errors are free to iterate through the errors themselves.
|
||||||
|
func (m MultiError) Cause() error { return nil }
|
||||||
|
|
||||||
|
func (m MultiError) Error() string {
|
||||||
|
s, n := "", 0
|
||||||
|
for _, e := range m {
|
||||||
|
if e != nil {
|
||||||
|
if n == 0 {
|
||||||
|
s = e.Error()
|
||||||
|
}
|
||||||
|
n++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch n {
|
||||||
|
case 0:
|
||||||
|
return "(0 errors)"
|
||||||
|
case 1:
|
||||||
|
return s
|
||||||
|
case 2:
|
||||||
|
return s + " (and 1 other error)"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s (and %d other errors)", s, n-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// any returns true if any element of m is an Error for which pred returns true.
|
||||||
|
func (m MultiError) any(pred func(Error) bool) bool {
|
||||||
|
for _, e := range m {
|
||||||
|
if ourErr, ok := e.(Error); ok && pred(ourErr) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
# This is the official list of gorilla/sessions authors for copyright purposes.
|
||||||
|
#
|
||||||
|
# Please keep the list sorted.
|
||||||
|
|
||||||
|
Ahmadreza Zibaei <ahmadrezazibaei@hotmail.com>
|
||||||
|
Anton Lindström <lindztr@gmail.com>
|
||||||
|
Brian Jones <mojobojo@gmail.com>
|
||||||
|
Collin Stedman <kronion@users.noreply.github.com>
|
||||||
|
Deniz Eren <dee.116@gmail.com>
|
||||||
|
Dmitry Chestnykh <dmitry@codingrobots.com>
|
||||||
|
Dustin Oprea <myselfasunder@gmail.com>
|
||||||
|
Egon Elbre <egonelbre@gmail.com>
|
||||||
|
enumappstore <appstore@enumapps.com>
|
||||||
|
Geofrey Ernest <geofreyernest@live.com>
|
||||||
|
Google LLC (https://opensource.google.com/)
|
||||||
|
Jerry Saravia <SaraviaJ@gmail.com>
|
||||||
|
Jonathan Gillham <jonathan.gillham@gamil.com>
|
||||||
|
Justin Clift <justin@postgresql.org>
|
||||||
|
Justin Hellings <justin.hellings@gmail.com>
|
||||||
|
Kamil Kisiel <kamil@kamilkisiel.net>
|
||||||
|
Keiji Yoshida <yoshida.keiji.84@gmail.com>
|
||||||
|
kliron <kliron@gmail.com>
|
||||||
|
Kshitij Saraogi <KshitijSaraogi@gmail.com>
|
||||||
|
Lauris BH <lauris@nix.lv>
|
||||||
|
Lukas Rist <glaslos@gmail.com>
|
||||||
|
Mark Dain <ancarda@users.noreply.github.com>
|
||||||
|
Matt Ho <matt.ho@gmail.com>
|
||||||
|
Matt Silverlock <matt@eatsleeprepeat.net>
|
||||||
|
Mattias Wadman <mattias.wadman@gmail.com>
|
||||||
|
Michael Schuett <michaeljs1990@gmail.com>
|
||||||
|
Michael Stapelberg <stapelberg@users.noreply.github.com>
|
||||||
|
Mirco Zeiss <mirco.zeiss@gmail.com>
|
||||||
|
moraes <rodrigo.moraes@gmail.com>
|
||||||
|
nvcnvn <nguyen@open-vn.org>
|
||||||
|
pappz <zoltan.pmail@gmail.com>
|
||||||
|
Pontus Leitzler <leitzler@users.noreply.github.com>
|
||||||
|
QuaSoft <info@quasoft.net>
|
||||||
|
rcadena <robert.cadena@gmail.com>
|
||||||
|
rodrigo moraes <rodrigo.moraes@gmail.com>
|
||||||
|
Shawn Smith <shawnpsmith@gmail.com>
|
||||||
|
Taylor Hurt <taylor.a.hurt@gmail.com>
|
||||||
|
Tortuoise <sanyasinp@gmail.com>
|
||||||
|
Vitor De Mario <vitordemario@gmail.com>
|
|
@ -0,0 +1,27 @@
|
||||||
|
Copyright (c) 2012-2018 The Gorilla Authors. All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are
|
||||||
|
met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
* Redistributions in binary form must reproduce the above
|
||||||
|
copyright notice, this list of conditions and the following disclaimer
|
||||||
|
in the documentation and/or other materials provided with the
|
||||||
|
distribution.
|
||||||
|
* Neither the name of Google Inc. nor the names of its
|
||||||
|
contributors may be used to endorse or promote products derived from
|
||||||
|
this software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||||
|
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
|
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||||
|
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||||
|
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@ -0,0 +1,90 @@
|
||||||
|
# sessions
|
||||||
|
|
||||||
|
[![GoDoc](https://godoc.org/github.com/gorilla/sessions?status.svg)](https://godoc.org/github.com/gorilla/sessions) [![Build Status](https://travis-ci.org/gorilla/sessions.svg?branch=master)](https://travis-ci.org/gorilla/sessions)
|
||||||
|
[![Sourcegraph](https://sourcegraph.com/github.com/gorilla/sessions/-/badge.svg)](https://sourcegraph.com/github.com/gorilla/sessions?badge)
|
||||||
|
|
||||||
|
gorilla/sessions provides cookie and filesystem sessions and infrastructure for
|
||||||
|
custom session backends.
|
||||||
|
|
||||||
|
The key features are:
|
||||||
|
|
||||||
|
- Simple API: use it as an easy way to set signed (and optionally
|
||||||
|
encrypted) cookies.
|
||||||
|
- Built-in backends to store sessions in cookies or the filesystem.
|
||||||
|
- Flash messages: session values that last until read.
|
||||||
|
- Convenient way to switch session persistency (aka "remember me") and set
|
||||||
|
other attributes.
|
||||||
|
- Mechanism to rotate authentication and encryption keys.
|
||||||
|
- Multiple sessions per request, even using different backends.
|
||||||
|
- Interfaces and infrastructure for custom session backends: sessions from
|
||||||
|
different stores can be retrieved and batch-saved using a common API.
|
||||||
|
|
||||||
|
Let's start with an example that shows the sessions API in a nutshell:
|
||||||
|
|
||||||
|
```go
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"github.com/gorilla/sessions"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Note: Don't store your key in your source code. Pass it via an
|
||||||
|
// environmental variable, or flag (or both), and don't accidentally commit it
|
||||||
|
// alongside your code. Ensure your key is sufficiently random - i.e. use Go's
|
||||||
|
// crypto/rand or securecookie.GenerateRandomKey(32) and persist the result.
|
||||||
|
var store = sessions.NewCookieStore([]byte(os.Getenv("SESSION_KEY")))
|
||||||
|
|
||||||
|
func MyHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Get a session. We're ignoring the error resulted from decoding an
|
||||||
|
// existing session: Get() always returns a session, even if empty.
|
||||||
|
session, _ := store.Get(r, "session-name")
|
||||||
|
// Set some session values.
|
||||||
|
session.Values["foo"] = "bar"
|
||||||
|
session.Values[42] = 43
|
||||||
|
// Save it before we write to the response/return from the handler.
|
||||||
|
err := session.Save(r, w)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
First we initialize a session store calling `NewCookieStore()` and passing a
|
||||||
|
secret key used to authenticate the session. Inside the handler, we call
|
||||||
|
`store.Get()` to retrieve an existing session or create a new one. Then we set
|
||||||
|
some session values in session.Values, which is a `map[interface{}]interface{}`.
|
||||||
|
And finally we call `session.Save()` to save the session in the response.
|
||||||
|
|
||||||
|
More examples are available [on the Gorilla
|
||||||
|
website](https://www.gorillatoolkit.org/pkg/sessions).
|
||||||
|
|
||||||
|
## Store Implementations
|
||||||
|
|
||||||
|
Other implementations of the `sessions.Store` interface:
|
||||||
|
|
||||||
|
- [github.com/starJammer/gorilla-sessions-arangodb](https://github.com/starJammer/gorilla-sessions-arangodb) - ArangoDB
|
||||||
|
- [github.com/yosssi/boltstore](https://github.com/yosssi/boltstore) - Bolt
|
||||||
|
- [github.com/srinathgs/couchbasestore](https://github.com/srinathgs/couchbasestore) - Couchbase
|
||||||
|
- [github.com/denizeren/dynamostore](https://github.com/denizeren/dynamostore) - Dynamodb on AWS
|
||||||
|
- [github.com/savaki/dynastore](https://github.com/savaki/dynastore) - DynamoDB on AWS (Official AWS library)
|
||||||
|
- [github.com/bradleypeabody/gorilla-sessions-memcache](https://github.com/bradleypeabody/gorilla-sessions-memcache) - Memcache
|
||||||
|
- [github.com/dsoprea/go-appengine-sessioncascade](https://github.com/dsoprea/go-appengine-sessioncascade) - Memcache/Datastore/Context in AppEngine
|
||||||
|
- [github.com/kidstuff/mongostore](https://github.com/kidstuff/mongostore) - MongoDB
|
||||||
|
- [github.com/srinathgs/mysqlstore](https://github.com/srinathgs/mysqlstore) - MySQL
|
||||||
|
- [github.com/EnumApps/clustersqlstore](https://github.com/EnumApps/clustersqlstore) - MySQL Cluster
|
||||||
|
- [github.com/antonlindstrom/pgstore](https://github.com/antonlindstrom/pgstore) - PostgreSQL
|
||||||
|
- [github.com/boj/redistore](https://github.com/boj/redistore) - Redis
|
||||||
|
- [github.com/rbcervilla/redisstore](https://github.com/rbcervilla/redisstore) - Redis (Single, Sentinel, Cluster)
|
||||||
|
- [github.com/boj/rethinkstore](https://github.com/boj/rethinkstore) - RethinkDB
|
||||||
|
- [github.com/boj/riakstore](https://github.com/boj/riakstore) - Riak
|
||||||
|
- [github.com/michaeljs1990/sqlitestore](https://github.com/michaeljs1990/sqlitestore) - SQLite
|
||||||
|
- [github.com/wader/gormstore](https://github.com/wader/gormstore) - GORM (MySQL, PostgreSQL, SQLite)
|
||||||
|
- [github.com/gernest/qlstore](https://github.com/gernest/qlstore) - ql
|
||||||
|
- [github.com/quasoft/memstore](https://github.com/quasoft/memstore) - In-memory implementation for use in unit tests
|
||||||
|
- [github.com/lafriks/xormstore](https://github.com/lafriks/xormstore) - XORM (MySQL, PostgreSQL, SQLite, Microsoft SQL Server, TiDB)
|
||||||
|
- [github.com/GoogleCloudPlatform/firestore-gorilla-sessions](https://github.com/GoogleCloudPlatform/firestore-gorilla-sessions) - Cloud Firestore
|
||||||
|
- [github.com/stephenafamo/crdbstore](https://github.com/stephenafamo/crdbstore) - CockroachDB
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
BSD licensed. See the LICENSE file for details.
|
|
@ -0,0 +1,19 @@
|
||||||
|
// +build !go1.11
|
||||||
|
|
||||||
|
package sessions
|
||||||
|
|
||||||
|
import "net/http"
|
||||||
|
|
||||||
|
// newCookieFromOptions returns an http.Cookie with the options set.
|
||||||
|
func newCookieFromOptions(name, value string, options *Options) *http.Cookie {
|
||||||
|
return &http.Cookie{
|
||||||
|
Name: name,
|
||||||
|
Value: value,
|
||||||
|
Path: options.Path,
|
||||||
|
Domain: options.Domain,
|
||||||
|
MaxAge: options.MaxAge,
|
||||||
|
Secure: options.Secure,
|
||||||
|
HttpOnly: options.HttpOnly,
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
// +build go1.11
|
||||||
|
|
||||||
|
package sessions
|
||||||
|
|
||||||
|
import "net/http"
|
||||||
|
|
||||||
|
// newCookieFromOptions returns an http.Cookie with the options set.
|
||||||
|
func newCookieFromOptions(name, value string, options *Options) *http.Cookie {
|
||||||
|
return &http.Cookie{
|
||||||
|
Name: name,
|
||||||
|
Value: value,
|
||||||
|
Path: options.Path,
|
||||||
|
Domain: options.Domain,
|
||||||
|
MaxAge: options.MaxAge,
|
||||||
|
Secure: options.Secure,
|
||||||
|
HttpOnly: options.HttpOnly,
|
||||||
|
SameSite: options.SameSite,
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,207 @@
|
||||||
|
// Copyright 2012 The Gorilla Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
/*
|
||||||
|
Package sessions provides cookie and filesystem sessions and
|
||||||
|
infrastructure for custom session backends.
|
||||||
|
|
||||||
|
The key features are:
|
||||||
|
|
||||||
|
* Simple API: use it as an easy way to set signed (and optionally
|
||||||
|
encrypted) cookies.
|
||||||
|
* Built-in backends to store sessions in cookies or the filesystem.
|
||||||
|
* Flash messages: session values that last until read.
|
||||||
|
* Convenient way to switch session persistency (aka "remember me") and set
|
||||||
|
other attributes.
|
||||||
|
* Mechanism to rotate authentication and encryption keys.
|
||||||
|
* Multiple sessions per request, even using different backends.
|
||||||
|
* Interfaces and infrastructure for custom session backends: sessions from
|
||||||
|
different stores can be retrieved and batch-saved using a common API.
|
||||||
|
|
||||||
|
Let's start with an example that shows the sessions API in a nutshell:
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"github.com/gorilla/sessions"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Note: Don't store your key in your source code. Pass it via an
|
||||||
|
// environmental variable, or flag (or both), and don't accidentally commit it
|
||||||
|
// alongside your code. Ensure your key is sufficiently random - i.e. use Go's
|
||||||
|
// crypto/rand or securecookie.GenerateRandomKey(32) and persist the result.
|
||||||
|
// Ensure SESSION_KEY exists in the environment, or sessions will fail.
|
||||||
|
var store = sessions.NewCookieStore([]byte(os.Getenv("SESSION_KEY")))
|
||||||
|
|
||||||
|
func MyHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Get a session. Get() always returns a session, even if empty.
|
||||||
|
session, err := store.Get(r, "session-name")
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set some session values.
|
||||||
|
session.Values["foo"] = "bar"
|
||||||
|
session.Values[42] = 43
|
||||||
|
// Save it before we write to the response/return from the handler.
|
||||||
|
err = session.Save(r, w)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
First we initialize a session store calling NewCookieStore() and passing a
|
||||||
|
secret key used to authenticate the session. Inside the handler, we call
|
||||||
|
store.Get() to retrieve an existing session or a new one. Then we set some
|
||||||
|
session values in session.Values, which is a map[interface{}]interface{}.
|
||||||
|
And finally we call session.Save() to save the session in the response.
|
||||||
|
|
||||||
|
Note that in production code, we should check for errors when calling
|
||||||
|
session.Save(r, w), and either display an error message or otherwise handle it.
|
||||||
|
|
||||||
|
Save must be called before writing to the response, otherwise the session
|
||||||
|
cookie will not be sent to the client.
|
||||||
|
|
||||||
|
That's all you need to know for the basic usage. Let's take a look at other
|
||||||
|
options, starting with flash messages.
|
||||||
|
|
||||||
|
Flash messages are session values that last until read. The term appeared with
|
||||||
|
Ruby On Rails a few years back. When we request a flash message, it is removed
|
||||||
|
from the session. To add a flash, call session.AddFlash(), and to get all
|
||||||
|
flashes, call session.Flashes(). Here is an example:
|
||||||
|
|
||||||
|
func MyHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Get a session.
|
||||||
|
session, err := store.Get(r, "session-name")
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the previous flashes, if any.
|
||||||
|
if flashes := session.Flashes(); len(flashes) > 0 {
|
||||||
|
// Use the flash values.
|
||||||
|
} else {
|
||||||
|
// Set a new flash.
|
||||||
|
session.AddFlash("Hello, flash messages world!")
|
||||||
|
}
|
||||||
|
err = session.Save(r, w)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Flash messages are useful to set information to be read after a redirection,
|
||||||
|
like after form submissions.
|
||||||
|
|
||||||
|
There may also be cases where you want to store a complex datatype within a
|
||||||
|
session, such as a struct. Sessions are serialised using the encoding/gob package,
|
||||||
|
so it is easy to register new datatypes for storage in sessions:
|
||||||
|
|
||||||
|
import(
|
||||||
|
"encoding/gob"
|
||||||
|
"github.com/gorilla/sessions"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Person struct {
|
||||||
|
FirstName string
|
||||||
|
LastName string
|
||||||
|
Email string
|
||||||
|
Age int
|
||||||
|
}
|
||||||
|
|
||||||
|
type M map[string]interface{}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
|
||||||
|
gob.Register(&Person{})
|
||||||
|
gob.Register(&M{})
|
||||||
|
}
|
||||||
|
|
||||||
|
As it's not possible to pass a raw type as a parameter to a function, gob.Register()
|
||||||
|
relies on us passing it a value of the desired type. In the example above we've passed
|
||||||
|
it a pointer to a struct and a pointer to a custom type representing a
|
||||||
|
map[string]interface. (We could have passed non-pointer values if we wished.) This will
|
||||||
|
then allow us to serialise/deserialise values of those types to and from our sessions.
|
||||||
|
|
||||||
|
Note that because session values are stored in a map[string]interface{}, there's
|
||||||
|
a need to type-assert data when retrieving it. We'll use the Person struct we registered above:
|
||||||
|
|
||||||
|
func MyHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
session, err := store.Get(r, "session-name")
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve our struct and type-assert it
|
||||||
|
val := session.Values["person"]
|
||||||
|
var person = &Person{}
|
||||||
|
if person, ok := val.(*Person); !ok {
|
||||||
|
// Handle the case that it's not an expected type
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now we can use our person object
|
||||||
|
}
|
||||||
|
|
||||||
|
By default, session cookies last for a month. This is probably too long for
|
||||||
|
some cases, but it is easy to change this and other attributes during
|
||||||
|
runtime. Sessions can be configured individually or the store can be
|
||||||
|
configured and then all sessions saved using it will use that configuration.
|
||||||
|
We access session.Options or store.Options to set a new configuration. The
|
||||||
|
fields are basically a subset of http.Cookie fields. Let's change the
|
||||||
|
maximum age of a session to one week:
|
||||||
|
|
||||||
|
session.Options = &sessions.Options{
|
||||||
|
Path: "/",
|
||||||
|
MaxAge: 86400 * 7,
|
||||||
|
HttpOnly: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
Sometimes we may want to change authentication and/or encryption keys without
|
||||||
|
breaking existing sessions. The CookieStore supports key rotation, and to use
|
||||||
|
it you just need to set multiple authentication and encryption keys, in pairs,
|
||||||
|
to be tested in order:
|
||||||
|
|
||||||
|
var store = sessions.NewCookieStore(
|
||||||
|
[]byte("new-authentication-key"),
|
||||||
|
[]byte("new-encryption-key"),
|
||||||
|
[]byte("old-authentication-key"),
|
||||||
|
[]byte("old-encryption-key"),
|
||||||
|
)
|
||||||
|
|
||||||
|
New sessions will be saved using the first pair. Old sessions can still be
|
||||||
|
read because the first pair will fail, and the second will be tested. This
|
||||||
|
makes it easy to "rotate" secret keys and still be able to validate existing
|
||||||
|
sessions. Note: for all pairs the encryption key is optional; set it to nil
|
||||||
|
or omit it and and encryption won't be used.
|
||||||
|
|
||||||
|
Multiple sessions can be used in the same request, even with different
|
||||||
|
session backends. When this happens, calling Save() on each session
|
||||||
|
individually would be cumbersome, so we have a way to save all sessions
|
||||||
|
at once: it's sessions.Save(). Here's an example:
|
||||||
|
|
||||||
|
var store = sessions.NewCookieStore([]byte("something-very-secret"))
|
||||||
|
|
||||||
|
func MyHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Get a session and set a value.
|
||||||
|
session1, _ := store.Get(r, "session-one")
|
||||||
|
session1.Values["foo"] = "bar"
|
||||||
|
// Get another session and set another value.
|
||||||
|
session2, _ := store.Get(r, "session-two")
|
||||||
|
session2.Values[42] = 43
|
||||||
|
// Save all sessions.
|
||||||
|
err = sessions.Save(r, w)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
This is possible because when we call Get() from a session store, it adds the
|
||||||
|
session to a common registry. Save() uses it to save all registered sessions.
|
||||||
|
*/
|
||||||
|
package sessions
|
|
@ -0,0 +1,102 @@
|
||||||
|
// This file contains code adapted from the Go standard library
|
||||||
|
// https://github.com/golang/go/blob/39ad0fd0789872f9469167be7fe9578625ff246e/src/net/http/lex.go
|
||||||
|
|
||||||
|
package sessions
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
var isTokenTable = [127]bool{
|
||||||
|
'!': true,
|
||||||
|
'#': true,
|
||||||
|
'$': true,
|
||||||
|
'%': true,
|
||||||
|
'&': true,
|
||||||
|
'\'': true,
|
||||||
|
'*': true,
|
||||||
|
'+': true,
|
||||||
|
'-': true,
|
||||||
|
'.': true,
|
||||||
|
'0': true,
|
||||||
|
'1': true,
|
||||||
|
'2': true,
|
||||||
|
'3': true,
|
||||||
|
'4': true,
|
||||||
|
'5': true,
|
||||||
|
'6': true,
|
||||||
|
'7': true,
|
||||||
|
'8': true,
|
||||||
|
'9': true,
|
||||||
|
'A': true,
|
||||||
|
'B': true,
|
||||||
|
'C': true,
|
||||||
|
'D': true,
|
||||||
|
'E': true,
|
||||||
|
'F': true,
|
||||||
|
'G': true,
|
||||||
|
'H': true,
|
||||||
|
'I': true,
|
||||||
|
'J': true,
|
||||||
|
'K': true,
|
||||||
|
'L': true,
|
||||||
|
'M': true,
|
||||||
|
'N': true,
|
||||||
|
'O': true,
|
||||||
|
'P': true,
|
||||||
|
'Q': true,
|
||||||
|
'R': true,
|
||||||
|
'S': true,
|
||||||
|
'T': true,
|
||||||
|
'U': true,
|
||||||
|
'W': true,
|
||||||
|
'V': true,
|
||||||
|
'X': true,
|
||||||
|
'Y': true,
|
||||||
|
'Z': true,
|
||||||
|
'^': true,
|
||||||
|
'_': true,
|
||||||
|
'`': true,
|
||||||
|
'a': true,
|
||||||
|
'b': true,
|
||||||
|
'c': true,
|
||||||
|
'd': true,
|
||||||
|
'e': true,
|
||||||
|
'f': true,
|
||||||
|
'g': true,
|
||||||
|
'h': true,
|
||||||
|
'i': true,
|
||||||
|
'j': true,
|
||||||
|
'k': true,
|
||||||
|
'l': true,
|
||||||
|
'm': true,
|
||||||
|
'n': true,
|
||||||
|
'o': true,
|
||||||
|
'p': true,
|
||||||
|
'q': true,
|
||||||
|
'r': true,
|
||||||
|
's': true,
|
||||||
|
't': true,
|
||||||
|
'u': true,
|
||||||
|
'v': true,
|
||||||
|
'w': true,
|
||||||
|
'x': true,
|
||||||
|
'y': true,
|
||||||
|
'z': true,
|
||||||
|
'|': true,
|
||||||
|
'~': true,
|
||||||
|
}
|
||||||
|
|
||||||
|
func isToken(r rune) bool {
|
||||||
|
i := int(r)
|
||||||
|
return i < len(isTokenTable) && isTokenTable[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
func isNotToken(r rune) bool {
|
||||||
|
return !isToken(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isCookieNameValid(raw string) bool {
|
||||||
|
if raw == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return strings.IndexFunc(raw, isNotToken) < 0
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
// +build !go1.11
|
||||||
|
|
||||||
|
package sessions
|
||||||
|
|
||||||
|
// Options stores configuration for a session or session store.
|
||||||
|
//
|
||||||
|
// Fields are a subset of http.Cookie fields.
|
||||||
|
type Options struct {
|
||||||
|
Path string
|
||||||
|
Domain string
|
||||||
|
// MaxAge=0 means no Max-Age attribute specified and the cookie will be
|
||||||
|
// deleted after the browser session ends.
|
||||||
|
// MaxAge<0 means delete cookie immediately.
|
||||||
|
// MaxAge>0 means Max-Age attribute present and given in seconds.
|
||||||
|
MaxAge int
|
||||||
|
Secure bool
|
||||||
|
HttpOnly bool
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
// +build go1.11
|
||||||
|
|
||||||
|
package sessions
|
||||||
|
|
||||||
|
import "net/http"
|
||||||
|
|
||||||
|
// Options stores configuration for a session or session store.
|
||||||
|
//
|
||||||
|
// Fields are a subset of http.Cookie fields.
|
||||||
|
type Options struct {
|
||||||
|
Path string
|
||||||
|
Domain string
|
||||||
|
// MaxAge=0 means no Max-Age attribute specified and the cookie will be
|
||||||
|
// deleted after the browser session ends.
|
||||||
|
// MaxAge<0 means delete cookie immediately.
|
||||||
|
// MaxAge>0 means Max-Age attribute present and given in seconds.
|
||||||
|
MaxAge int
|
||||||
|
Secure bool
|
||||||
|
HttpOnly bool
|
||||||
|
// Defaults to http.SameSiteDefaultMode
|
||||||
|
SameSite http.SameSite
|
||||||
|
}
|
|
@ -0,0 +1,218 @@
|
||||||
|
// Copyright 2012 The Gorilla Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package sessions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/gob"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Default flashes key.
|
||||||
|
const flashesKey = "_flash"
|
||||||
|
|
||||||
|
// Session --------------------------------------------------------------------
|
||||||
|
|
||||||
|
// NewSession is called by session stores to create a new session instance.
|
||||||
|
func NewSession(store Store, name string) *Session {
|
||||||
|
return &Session{
|
||||||
|
Values: make(map[interface{}]interface{}),
|
||||||
|
store: store,
|
||||||
|
name: name,
|
||||||
|
Options: new(Options),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Session stores the values and optional configuration for a session.
|
||||||
|
type Session struct {
|
||||||
|
// The ID of the session, generated by stores. It should not be used for
|
||||||
|
// user data.
|
||||||
|
ID string
|
||||||
|
// Values contains the user-data for the session.
|
||||||
|
Values map[interface{}]interface{}
|
||||||
|
Options *Options
|
||||||
|
IsNew bool
|
||||||
|
store Store
|
||||||
|
name string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flashes returns a slice of flash messages from the session.
|
||||||
|
//
|
||||||
|
// A single variadic argument is accepted, and it is optional: it defines
|
||||||
|
// the flash key. If not defined "_flash" is used by default.
|
||||||
|
func (s *Session) Flashes(vars ...string) []interface{} {
|
||||||
|
var flashes []interface{}
|
||||||
|
key := flashesKey
|
||||||
|
if len(vars) > 0 {
|
||||||
|
key = vars[0]
|
||||||
|
}
|
||||||
|
if v, ok := s.Values[key]; ok {
|
||||||
|
// Drop the flashes and return it.
|
||||||
|
delete(s.Values, key)
|
||||||
|
flashes = v.([]interface{})
|
||||||
|
}
|
||||||
|
return flashes
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddFlash adds a flash message to the session.
|
||||||
|
//
|
||||||
|
// A single variadic argument is accepted, and it is optional: it defines
|
||||||
|
// the flash key. If not defined "_flash" is used by default.
|
||||||
|
func (s *Session) AddFlash(value interface{}, vars ...string) {
|
||||||
|
key := flashesKey
|
||||||
|
if len(vars) > 0 {
|
||||||
|
key = vars[0]
|
||||||
|
}
|
||||||
|
var flashes []interface{}
|
||||||
|
if v, ok := s.Values[key]; ok {
|
||||||
|
flashes = v.([]interface{})
|
||||||
|
}
|
||||||
|
s.Values[key] = append(flashes, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save is a convenience method to save this session. It is the same as calling
|
||||||
|
// store.Save(request, response, session). You should call Save before writing to
|
||||||
|
// the response or returning from the handler.
|
||||||
|
func (s *Session) Save(r *http.Request, w http.ResponseWriter) error {
|
||||||
|
return s.store.Save(r, w, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name returns the name used to register the session.
|
||||||
|
func (s *Session) Name() string {
|
||||||
|
return s.name
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store returns the session store used to register the session.
|
||||||
|
func (s *Session) Store() Store {
|
||||||
|
return s.store
|
||||||
|
}
|
||||||
|
|
||||||
|
// Registry -------------------------------------------------------------------
|
||||||
|
|
||||||
|
// sessionInfo stores a session tracked by the registry.
|
||||||
|
type sessionInfo struct {
|
||||||
|
s *Session
|
||||||
|
e error
|
||||||
|
}
|
||||||
|
|
||||||
|
// contextKey is the type used to store the registry in the context.
|
||||||
|
type contextKey int
|
||||||
|
|
||||||
|
// registryKey is the key used to store the registry in the context.
|
||||||
|
const registryKey contextKey = 0
|
||||||
|
|
||||||
|
// GetRegistry returns a registry instance for the current request.
|
||||||
|
func GetRegistry(r *http.Request) *Registry {
|
||||||
|
var ctx = r.Context()
|
||||||
|
registry := ctx.Value(registryKey)
|
||||||
|
if registry != nil {
|
||||||
|
return registry.(*Registry)
|
||||||
|
}
|
||||||
|
newRegistry := &Registry{
|
||||||
|
request: r,
|
||||||
|
sessions: make(map[string]sessionInfo),
|
||||||
|
}
|
||||||
|
*r = *r.WithContext(context.WithValue(ctx, registryKey, newRegistry))
|
||||||
|
return newRegistry
|
||||||
|
}
|
||||||
|
|
||||||
|
// Registry stores sessions used during a request.
|
||||||
|
type Registry struct {
|
||||||
|
request *http.Request
|
||||||
|
sessions map[string]sessionInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get registers and returns a session for the given name and session store.
|
||||||
|
//
|
||||||
|
// It returns a new session if there are no sessions registered for the name.
|
||||||
|
func (s *Registry) Get(store Store, name string) (session *Session, err error) {
|
||||||
|
if !isCookieNameValid(name) {
|
||||||
|
return nil, fmt.Errorf("sessions: invalid character in cookie name: %s", name)
|
||||||
|
}
|
||||||
|
if info, ok := s.sessions[name]; ok {
|
||||||
|
session, err = info.s, info.e
|
||||||
|
} else {
|
||||||
|
session, err = store.New(s.request, name)
|
||||||
|
session.name = name
|
||||||
|
s.sessions[name] = sessionInfo{s: session, e: err}
|
||||||
|
}
|
||||||
|
session.store = store
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save saves all sessions registered for the current request.
|
||||||
|
func (s *Registry) Save(w http.ResponseWriter) error {
|
||||||
|
var errMulti MultiError
|
||||||
|
for name, info := range s.sessions {
|
||||||
|
session := info.s
|
||||||
|
if session.store == nil {
|
||||||
|
errMulti = append(errMulti, fmt.Errorf(
|
||||||
|
"sessions: missing store for session %q", name))
|
||||||
|
} else if err := session.store.Save(s.request, w, session); err != nil {
|
||||||
|
errMulti = append(errMulti, fmt.Errorf(
|
||||||
|
"sessions: error saving session %q -- %v", name, err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if errMulti != nil {
|
||||||
|
return errMulti
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helpers --------------------------------------------------------------------
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
gob.Register([]interface{}{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save saves all sessions used during the current request.
|
||||||
|
func Save(r *http.Request, w http.ResponseWriter) error {
|
||||||
|
return GetRegistry(r).Save(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCookie returns an http.Cookie with the options set. It also sets
|
||||||
|
// the Expires field calculated based on the MaxAge value, for Internet
|
||||||
|
// Explorer compatibility.
|
||||||
|
func NewCookie(name, value string, options *Options) *http.Cookie {
|
||||||
|
cookie := newCookieFromOptions(name, value, options)
|
||||||
|
if options.MaxAge > 0 {
|
||||||
|
d := time.Duration(options.MaxAge) * time.Second
|
||||||
|
cookie.Expires = time.Now().Add(d)
|
||||||
|
} else if options.MaxAge < 0 {
|
||||||
|
// Set it to the past to expire now.
|
||||||
|
cookie.Expires = time.Unix(1, 0)
|
||||||
|
}
|
||||||
|
return cookie
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
// MultiError stores multiple errors.
|
||||||
|
//
|
||||||
|
// Borrowed from the App Engine SDK.
|
||||||
|
type MultiError []error
|
||||||
|
|
||||||
|
func (m MultiError) Error() string {
|
||||||
|
s, n := "", 0
|
||||||
|
for _, e := range m {
|
||||||
|
if e != nil {
|
||||||
|
if n == 0 {
|
||||||
|
s = e.Error()
|
||||||
|
}
|
||||||
|
n++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch n {
|
||||||
|
case 0:
|
||||||
|
return "(0 errors)"
|
||||||
|
case 1:
|
||||||
|
return s
|
||||||
|
case 2:
|
||||||
|
return s + " (and 1 other error)"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s (and %d other errors)", s, n-1)
|
||||||
|
}
|
|
@ -0,0 +1,292 @@
|
||||||
|
// Copyright 2012 The Gorilla Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package sessions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base32"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/gorilla/securecookie"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Store is an interface for custom session stores.
|
||||||
|
//
|
||||||
|
// See CookieStore and FilesystemStore for examples.
|
||||||
|
type Store interface {
|
||||||
|
// Get should return a cached session.
|
||||||
|
Get(r *http.Request, name string) (*Session, error)
|
||||||
|
|
||||||
|
// New should create and return a new session.
|
||||||
|
//
|
||||||
|
// Note that New should never return a nil session, even in the case of
|
||||||
|
// an error if using the Registry infrastructure to cache the session.
|
||||||
|
New(r *http.Request, name string) (*Session, error)
|
||||||
|
|
||||||
|
// Save should persist session to the underlying store implementation.
|
||||||
|
Save(r *http.Request, w http.ResponseWriter, s *Session) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// CookieStore ----------------------------------------------------------------
|
||||||
|
|
||||||
|
// NewCookieStore returns a new CookieStore.
|
||||||
|
//
|
||||||
|
// Keys are defined in pairs to allow key rotation, but the common case is
|
||||||
|
// to set a single authentication key and optionally an encryption key.
|
||||||
|
//
|
||||||
|
// The first key in a pair is used for authentication and the second for
|
||||||
|
// encryption. The encryption key can be set to nil or omitted in the last
|
||||||
|
// pair, but the authentication key is required in all pairs.
|
||||||
|
//
|
||||||
|
// It is recommended to use an authentication key with 32 or 64 bytes.
|
||||||
|
// The encryption key, if set, must be either 16, 24, or 32 bytes to select
|
||||||
|
// AES-128, AES-192, or AES-256 modes.
|
||||||
|
func NewCookieStore(keyPairs ...[]byte) *CookieStore {
|
||||||
|
cs := &CookieStore{
|
||||||
|
Codecs: securecookie.CodecsFromPairs(keyPairs...),
|
||||||
|
Options: &Options{
|
||||||
|
Path: "/",
|
||||||
|
MaxAge: 86400 * 30,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cs.MaxAge(cs.Options.MaxAge)
|
||||||
|
return cs
|
||||||
|
}
|
||||||
|
|
||||||
|
// CookieStore stores sessions using secure cookies.
|
||||||
|
type CookieStore struct {
|
||||||
|
Codecs []securecookie.Codec
|
||||||
|
Options *Options // default configuration
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get returns a session for the given name after adding it to the registry.
|
||||||
|
//
|
||||||
|
// It returns a new session if the sessions doesn't exist. Access IsNew on
|
||||||
|
// the session to check if it is an existing session or a new one.
|
||||||
|
//
|
||||||
|
// It returns a new session and an error if the session exists but could
|
||||||
|
// not be decoded.
|
||||||
|
func (s *CookieStore) Get(r *http.Request, name string) (*Session, error) {
|
||||||
|
return GetRegistry(r).Get(s, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// New returns a session for the given name without adding it to the registry.
|
||||||
|
//
|
||||||
|
// The difference between New() and Get() is that calling New() twice will
|
||||||
|
// decode the session data twice, while Get() registers and reuses the same
|
||||||
|
// decoded session after the first call.
|
||||||
|
func (s *CookieStore) New(r *http.Request, name string) (*Session, error) {
|
||||||
|
session := NewSession(s, name)
|
||||||
|
opts := *s.Options
|
||||||
|
session.Options = &opts
|
||||||
|
session.IsNew = true
|
||||||
|
var err error
|
||||||
|
if c, errCookie := r.Cookie(name); errCookie == nil {
|
||||||
|
err = securecookie.DecodeMulti(name, c.Value, &session.Values,
|
||||||
|
s.Codecs...)
|
||||||
|
if err == nil {
|
||||||
|
session.IsNew = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return session, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save adds a single session to the response.
|
||||||
|
func (s *CookieStore) Save(r *http.Request, w http.ResponseWriter,
|
||||||
|
session *Session) error {
|
||||||
|
encoded, err := securecookie.EncodeMulti(session.Name(), session.Values,
|
||||||
|
s.Codecs...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
http.SetCookie(w, NewCookie(session.Name(), encoded, session.Options))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MaxAge sets the maximum age for the store and the underlying cookie
|
||||||
|
// implementation. Individual sessions can be deleted by setting Options.MaxAge
|
||||||
|
// = -1 for that session.
|
||||||
|
func (s *CookieStore) MaxAge(age int) {
|
||||||
|
s.Options.MaxAge = age
|
||||||
|
|
||||||
|
// Set the maxAge for each securecookie instance.
|
||||||
|
for _, codec := range s.Codecs {
|
||||||
|
if sc, ok := codec.(*securecookie.SecureCookie); ok {
|
||||||
|
sc.MaxAge(age)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FilesystemStore ------------------------------------------------------------
|
||||||
|
|
||||||
|
var fileMutex sync.RWMutex
|
||||||
|
|
||||||
|
// NewFilesystemStore returns a new FilesystemStore.
|
||||||
|
//
|
||||||
|
// The path argument is the directory where sessions will be saved. If empty
|
||||||
|
// it will use os.TempDir().
|
||||||
|
//
|
||||||
|
// See NewCookieStore() for a description of the other parameters.
|
||||||
|
func NewFilesystemStore(path string, keyPairs ...[]byte) *FilesystemStore {
|
||||||
|
if path == "" {
|
||||||
|
path = os.TempDir()
|
||||||
|
}
|
||||||
|
fs := &FilesystemStore{
|
||||||
|
Codecs: securecookie.CodecsFromPairs(keyPairs...),
|
||||||
|
Options: &Options{
|
||||||
|
Path: "/",
|
||||||
|
MaxAge: 86400 * 30,
|
||||||
|
},
|
||||||
|
path: path,
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.MaxAge(fs.Options.MaxAge)
|
||||||
|
return fs
|
||||||
|
}
|
||||||
|
|
||||||
|
// FilesystemStore stores sessions in the filesystem.
|
||||||
|
//
|
||||||
|
// It also serves as a reference for custom stores.
|
||||||
|
//
|
||||||
|
// This store is still experimental and not well tested. Feedback is welcome.
|
||||||
|
type FilesystemStore struct {
|
||||||
|
Codecs []securecookie.Codec
|
||||||
|
Options *Options // default configuration
|
||||||
|
path string
|
||||||
|
}
|
||||||
|
|
||||||
|
// MaxLength restricts the maximum length of new sessions to l.
|
||||||
|
// If l is 0 there is no limit to the size of a session, use with caution.
|
||||||
|
// The default for a new FilesystemStore is 4096.
|
||||||
|
func (s *FilesystemStore) MaxLength(l int) {
|
||||||
|
for _, c := range s.Codecs {
|
||||||
|
if codec, ok := c.(*securecookie.SecureCookie); ok {
|
||||||
|
codec.MaxLength(l)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get returns a session for the given name after adding it to the registry.
|
||||||
|
//
|
||||||
|
// See CookieStore.Get().
|
||||||
|
func (s *FilesystemStore) Get(r *http.Request, name string) (*Session, error) {
|
||||||
|
return GetRegistry(r).Get(s, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// New returns a session for the given name without adding it to the registry.
|
||||||
|
//
|
||||||
|
// See CookieStore.New().
|
||||||
|
func (s *FilesystemStore) New(r *http.Request, name string) (*Session, error) {
|
||||||
|
session := NewSession(s, name)
|
||||||
|
opts := *s.Options
|
||||||
|
session.Options = &opts
|
||||||
|
session.IsNew = true
|
||||||
|
var err error
|
||||||
|
if c, errCookie := r.Cookie(name); errCookie == nil {
|
||||||
|
err = securecookie.DecodeMulti(name, c.Value, &session.ID, s.Codecs...)
|
||||||
|
if err == nil {
|
||||||
|
err = s.load(session)
|
||||||
|
if err == nil {
|
||||||
|
session.IsNew = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return session, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save adds a single session to the response.
|
||||||
|
//
|
||||||
|
// If the Options.MaxAge of the session is <= 0 then the session file will be
|
||||||
|
// deleted from the store path. With this process it enforces the properly
|
||||||
|
// session cookie handling so no need to trust in the cookie management in the
|
||||||
|
// web browser.
|
||||||
|
func (s *FilesystemStore) Save(r *http.Request, w http.ResponseWriter,
|
||||||
|
session *Session) error {
|
||||||
|
// Delete if max-age is <= 0
|
||||||
|
if session.Options.MaxAge <= 0 {
|
||||||
|
if err := s.erase(session); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
http.SetCookie(w, NewCookie(session.Name(), "", session.Options))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if session.ID == "" {
|
||||||
|
// Because the ID is used in the filename, encode it to
|
||||||
|
// use alphanumeric characters only.
|
||||||
|
session.ID = strings.TrimRight(
|
||||||
|
base32.StdEncoding.EncodeToString(
|
||||||
|
securecookie.GenerateRandomKey(32)), "=")
|
||||||
|
}
|
||||||
|
if err := s.save(session); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
encoded, err := securecookie.EncodeMulti(session.Name(), session.ID,
|
||||||
|
s.Codecs...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
http.SetCookie(w, NewCookie(session.Name(), encoded, session.Options))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MaxAge sets the maximum age for the store and the underlying cookie
|
||||||
|
// implementation. Individual sessions can be deleted by setting Options.MaxAge
|
||||||
|
// = -1 for that session.
|
||||||
|
func (s *FilesystemStore) MaxAge(age int) {
|
||||||
|
s.Options.MaxAge = age
|
||||||
|
|
||||||
|
// Set the maxAge for each securecookie instance.
|
||||||
|
for _, codec := range s.Codecs {
|
||||||
|
if sc, ok := codec.(*securecookie.SecureCookie); ok {
|
||||||
|
sc.MaxAge(age)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// save writes encoded session.Values to a file.
|
||||||
|
func (s *FilesystemStore) save(session *Session) error {
|
||||||
|
encoded, err := securecookie.EncodeMulti(session.Name(), session.Values,
|
||||||
|
s.Codecs...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
filename := filepath.Join(s.path, "session_"+session.ID)
|
||||||
|
fileMutex.Lock()
|
||||||
|
defer fileMutex.Unlock()
|
||||||
|
return ioutil.WriteFile(filename, []byte(encoded), 0600)
|
||||||
|
}
|
||||||
|
|
||||||
|
// load reads a file and decodes its content into session.Values.
|
||||||
|
func (s *FilesystemStore) load(session *Session) error {
|
||||||
|
filename := filepath.Join(s.path, "session_"+session.ID)
|
||||||
|
fileMutex.RLock()
|
||||||
|
defer fileMutex.RUnlock()
|
||||||
|
fdata, err := ioutil.ReadFile(filename)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = securecookie.DecodeMulti(session.Name(), string(fdata),
|
||||||
|
&session.Values, s.Codecs...); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete session file
|
||||||
|
func (s *FilesystemStore) erase(session *Session) error {
|
||||||
|
filename := filepath.Join(s.path, "session_"+session.ID)
|
||||||
|
|
||||||
|
fileMutex.RLock()
|
||||||
|
defer fileMutex.RUnlock()
|
||||||
|
|
||||||
|
err := os.Remove(filename)
|
||||||
|
return err
|
||||||
|
}
|
|
@ -351,6 +351,12 @@ github.com/google/gopacket/routing
|
||||||
# github.com/google/uuid v1.3.0
|
# github.com/google/uuid v1.3.0
|
||||||
## explicit
|
## explicit
|
||||||
github.com/google/uuid
|
github.com/google/uuid
|
||||||
|
# github.com/gorilla/securecookie v1.1.1
|
||||||
|
## explicit
|
||||||
|
github.com/gorilla/securecookie
|
||||||
|
# github.com/gorilla/sessions v1.2.1
|
||||||
|
## explicit
|
||||||
|
github.com/gorilla/sessions
|
||||||
# github.com/gorilla/websocket v1.5.0
|
# github.com/gorilla/websocket v1.5.0
|
||||||
## explicit; go 1.12
|
## explicit; go 1.12
|
||||||
github.com/gorilla/websocket
|
github.com/gorilla/websocket
|
||||||
|
|
Loading…
Reference in New Issue