-
Notifications
You must be signed in to change notification settings - Fork 0
/
cqlstore.go
176 lines (142 loc) · 4.74 KB
/
cqlstore.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
// Package cqlstore provides an Apache Cassandra implementation of HTTP session
// storage for github.com/gorilla/sessions.
package cqlstore
import (
"errors"
"net/http"
"regexp"
"time"
"github.com/gocql/gocql"
"github.com/gorilla/securecookie"
"github.com/gorilla/sessions"
)
// CQLStore provides a Cassandra backed implementation of the interface Store
// from github.com/gorilla/sessions
type CQLStore struct {
Options *sessions.Options
Codecs []securecookie.Codec
saveQ *gocql.Query
deleteQ *gocql.Query
loadQ *gocql.Query
}
// New creates a new CQLStore. It requires an active gocql.Session and the name
// of the table where it should store session data. It will create this table
// with the appropriate schema if it does not exist. Additionally pass one or
// more byte slices to serve as authentication and/or encryption keys for both
// the cookie's session ID value and the values stored in the database.
func New(cs *gocql.Session, table string, keypairs ...[]byte) (*CQLStore, error) {
var err error
re := regexp.MustCompile("^[a-zA-Z0-9_]+$")
if !re.MatchString(table) {
return &CQLStore{}, errors.New("Invalid table name " + table)
}
// TODO add more columns for timestamps?
create := `
CREATE TABLE IF NOT EXISTS "` + table + `" (
id uuid,
data text,
PRIMARY KEY (id)
)`
err = cs.Query(create, table).Exec()
if err != nil {
return &CQLStore{}, createError{err}
}
st := &CQLStore{
Options: &sessions.Options{
Path: "/",
MaxAge: 86400 * 30,
},
Codecs: securecookie.CodecsFromPairs(keypairs...),
saveQ: cs.Query(`INSERT INTO "` + table + `" ("id", "data") VALUES(?, ?) USING TTL ?`),
deleteQ: cs.Query(`DELETE FROM "` + table + `" WHERE "id" = ?`),
loadQ: cs.Query(`SELECT "data" FROM "` + table + `" WHERE "id" = ?`),
}
return st, nil
}
// Get creates or returns a session from the request registry. It never returns
// a nil session.
func (st *CQLStore) Get(r *http.Request, name string) (*sessions.Session, error) {
return sessions.GetRegistry(r).Get(st, name)
}
// New creates and returns a new session without adding it to the registry. If
// the request has the named cookie then it will decode the session ID and load
// session values from the database. If the request might already have had the
// session loaded then calling Get instead will be faster. It never returns a
// nil session.
func (st *CQLStore) New(r *http.Request, name string) (*sessions.Session, error) {
s := sessions.NewSession(st, name)
s.IsNew = true
// Copy options from store to session
opts := *st.Options
s.Options = &opts
// See if the request has a cookie for this session. If it does not we can
// just return the new session struct.
c, errCookie := r.Cookie(name)
if errCookie != nil {
return s, nil
}
// Okay so the request identified a session. Try to load it.
// Decode the cookie value into the session id
if err := securecookie.DecodeMulti(name, c.Value, &s.ID, st.Codecs...); err != nil {
return s, loadError{err}
}
var encData string
if err := st.loadQ.Bind(s.ID).Scan(&encData); err != nil {
return s, loadError{err}
}
if err := securecookie.DecodeMulti(s.Name(), encData, &s.Values, st.Codecs...); err != nil {
return s, loadError{err}
}
s.IsNew = false
return s, nil
}
// Save persists session values to the database and adds the session ID cookie
// to the request. Save must be called before writing the response or the
// cookie will not be sent.
func (st *CQLStore) Save(r *http.Request, w http.ResponseWriter, s *sessions.Session) error {
if s.Options.MaxAge < 0 {
if err := st.deleteQ.Bind(s.ID).Exec(); err != nil {
return saveError{err}
}
http.SetCookie(w, sessions.NewCookie(s.Name(), "", s.Options))
return nil
}
if s.ID == "" {
// TODO is there a better one to use here?
s.ID = gocql.UUIDFromTime(time.Now()).String()
}
// Encode the data to store in the db
encData, err := securecookie.EncodeMulti(s.Name(), s.Values, st.Codecs...)
if err != nil {
return saveError{err}
}
if err := st.saveQ.Bind(s.ID, encData, st.Options.MaxAge).Exec(); err != nil {
return saveError{err}
}
// Encode the session ID and set it in a cookie
encID, err := securecookie.EncodeMulti(s.Name(), s.ID, st.Codecs...)
if err != nil {
return saveError{err}
}
http.SetCookie(w, sessions.NewCookie(s.Name(), encID, s.Options))
return nil
}
// TODO better error handling
type createError struct {
err error
}
func (e createError) Error() string {
return "Could not create sessions table. Error: " + e.err.Error()
}
type saveError struct {
err error
}
func (e saveError) Error() string {
return "Could not save session data. Error: " + e.err.Error()
}
type loadError struct {
err error
}
func (e loadError) Error() string {
return "Could not load session data. Error: " + e.err.Error()
}