├── LICENSE ├── README.md └── sessions.go /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 The Gorilla Authors. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name of Google Inc. nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | gaesessions 2 | =========== 3 | 4 | session stores for Google App Engine's datastore and memcahe. 5 | -------------------------------------------------------------------------------- /sessions.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 The Gorilla Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package gaesessions 6 | 7 | import ( 8 | "bytes" 9 | "encoding/base32" 10 | "encoding/gob" 11 | "net/http" 12 | "strings" 13 | "time" 14 | 15 | "appengine" 16 | "appengine/datastore" 17 | "appengine/memcache" 18 | 19 | "github.com/gorilla/securecookie" 20 | "github.com/gorilla/sessions" 21 | ) 22 | 23 | // MemcacheDatastoreStore ----------------------------------------------------- 24 | 25 | const DefaultNonPersistentSessionDuration = time.Duration(24) * time.Hour 26 | const defaultKind = "Session" 27 | 28 | // NewMemcacheDatastoreStore returns a new MemcacheDatastoreStore. 29 | // 30 | // The kind argument is the kind name used to store the session data. 31 | // If empty it will use "Session". 32 | // 33 | // See NewCookieStore() for a description of the other parameters. 34 | func NewMemcacheDatastoreStore(kind, keyPrefix string, nonPersistentSessionDuration time.Duration, keyPairs ...[]byte) *MemcacheDatastoreStore { 35 | if kind == "" { 36 | kind = defaultKind 37 | } 38 | if keyPrefix == "" { 39 | keyPrefix = "gorilla.appengine.sessions." 40 | } 41 | return &MemcacheDatastoreStore{ 42 | Codecs: securecookie.CodecsFromPairs(keyPairs...), 43 | Options: &sessions.Options{ 44 | Path: "/", 45 | MaxAge: 86400 * 30, 46 | }, 47 | kind: kind, 48 | prefix: keyPrefix, 49 | nonPersistentSessionDuration: nonPersistentSessionDuration, 50 | } 51 | } 52 | 53 | type MemcacheDatastoreStore struct { 54 | Codecs []securecookie.Codec 55 | Options *sessions.Options // default configuration 56 | kind string 57 | prefix string 58 | nonPersistentSessionDuration time.Duration 59 | } 60 | 61 | // Get returns a session for the given name after adding it to the registry. 62 | // 63 | // See CookieStore.Get(). 64 | func (s *MemcacheDatastoreStore) Get(r *http.Request, name string) ( 65 | *sessions.Session, error) { 66 | return sessions.GetRegistry(r).Get(s, name) 67 | } 68 | 69 | // New returns a session for the given name without adding it to the registry. 70 | // 71 | // See CookieStore.New(). 72 | func (s *MemcacheDatastoreStore) New(r *http.Request, name string) (*sessions.Session, 73 | error) { 74 | session := sessions.NewSession(s, name) 75 | session.Options = &(*s.Options) 76 | session.IsNew = true 77 | var err error 78 | if cookie, errCookie := r.Cookie(name); errCookie == nil { 79 | err = securecookie.DecodeMulti(name, cookie.Value, &session.ID, 80 | s.Codecs...) 81 | if err == nil { 82 | c := appengine.NewContext(r) 83 | err = loadFromMemcache(c, session) 84 | if err == memcache.ErrCacheMiss { 85 | err = loadFromDatastore(c, s.kind, session) 86 | } 87 | if err == nil { 88 | session.IsNew = false 89 | } 90 | } 91 | } 92 | return session, err 93 | } 94 | 95 | // Save adds a single session to the response. 96 | func (s *MemcacheDatastoreStore) Save(r *http.Request, w http.ResponseWriter, 97 | session *sessions.Session) error { 98 | if session.ID == "" { 99 | session.ID = s.prefix + 100 | strings.TrimRight( 101 | base32.StdEncoding.EncodeToString( 102 | securecookie.GenerateRandomKey(32)), "=") 103 | } 104 | c := appengine.NewContext(r) 105 | if err := saveToMemcache(c, s.nonPersistentSessionDuration, session); err != nil { 106 | return err 107 | } 108 | if err := saveToDatastore(c, s.kind, s.nonPersistentSessionDuration, session); err != nil { 109 | return err 110 | } 111 | encoded, err := securecookie.EncodeMulti(session.Name(), session.ID, 112 | s.Codecs...) 113 | if err != nil { 114 | return err 115 | } 116 | http.SetCookie(w, sessions.NewCookie(session.Name(), encoded, 117 | session.Options)) 118 | return nil 119 | } 120 | 121 | // DatastoreStore ------------------------------------------------------------- 122 | 123 | // Session is used to load and save session data in the datastore. 124 | type Session struct { 125 | Date time.Time 126 | ExpirationDate time.Time 127 | Value []byte 128 | } 129 | 130 | // NewDatastoreStore returns a new DatastoreStore. 131 | // 132 | // The kind argument is the kind name used to store the session data. 133 | // If empty it will use "Session". 134 | // 135 | // See NewCookieStore() for a description of the other parameters. 136 | func NewDatastoreStore(kind string, nonPersistentSessionDuration time.Duration, keyPairs ...[]byte) *DatastoreStore { 137 | if kind == "" { 138 | kind = "Session" 139 | } 140 | return &DatastoreStore{ 141 | Codecs: securecookie.CodecsFromPairs(keyPairs...), 142 | Options: &sessions.Options{ 143 | Path: "/", 144 | MaxAge: 86400 * 30, 145 | }, 146 | kind: kind, 147 | nonPersistentSessionDuration: nonPersistentSessionDuration, 148 | } 149 | } 150 | 151 | // DatastoreStore stores sessions in the App Engine datastore. 152 | type DatastoreStore struct { 153 | Codecs []securecookie.Codec 154 | Options *sessions.Options // default configuration 155 | kind string 156 | nonPersistentSessionDuration time.Duration 157 | } 158 | 159 | // Get returns a session for the given name after adding it to the registry. 160 | // 161 | // See CookieStore.Get(). 162 | func (s *DatastoreStore) Get(r *http.Request, name string) (*sessions.Session, 163 | error) { 164 | return sessions.GetRegistry(r).Get(s, name) 165 | } 166 | 167 | // New returns a session for the given name without adding it to the registry. 168 | // 169 | // See CookieStore.New(). 170 | func (s *DatastoreStore) New(r *http.Request, name string) (*sessions.Session, 171 | error) { 172 | session := sessions.NewSession(s, name) 173 | session.Options = &(*s.Options) 174 | session.IsNew = true 175 | var err error 176 | if cookie, errCookie := r.Cookie(name); errCookie == nil { 177 | err = securecookie.DecodeMulti(name, cookie.Value, &session.ID, 178 | s.Codecs...) 179 | if err == nil { 180 | c := appengine.NewContext(r) 181 | err = loadFromDatastore(c, s.kind, session) 182 | if err == nil { 183 | session.IsNew = false 184 | } 185 | } 186 | } 187 | return session, err 188 | } 189 | 190 | // Save adds a single session to the response. 191 | func (s *DatastoreStore) Save(r *http.Request, w http.ResponseWriter, 192 | session *sessions.Session) error { 193 | if session.ID == "" { 194 | session.ID = 195 | strings.TrimRight( 196 | base32.StdEncoding.EncodeToString( 197 | securecookie.GenerateRandomKey(32)), "=") 198 | } 199 | c := appengine.NewContext(r) 200 | if err := saveToDatastore(c, s.kind, s.nonPersistentSessionDuration, session); err != nil { 201 | return err 202 | } 203 | encoded, err := securecookie.EncodeMulti(session.Name(), session.ID, 204 | s.Codecs...) 205 | if err != nil { 206 | return err 207 | } 208 | http.SetCookie(w, sessions.NewCookie(session.Name(), encoded, 209 | session.Options)) 210 | return nil 211 | } 212 | 213 | // save writes encoded session.Values to datastore. 214 | func saveToDatastore(c appengine.Context, kind string, 215 | nonPersistentSessionDuration time.Duration, 216 | session *sessions.Session) error { 217 | if len(session.Values) == 0 { 218 | // Don't need to write anything. 219 | return nil 220 | } 221 | serialized, err := serialize(session.Values) 222 | if err != nil { 223 | return err 224 | } 225 | k := datastore.NewKey(c, kind, session.ID, 0, nil) 226 | now := time.Now() 227 | var expirationDate time.Time 228 | var expiration time.Duration 229 | if session.Options.MaxAge > 0 { 230 | expiration = time.Duration(session.Options.MaxAge) * time.Second 231 | } else { 232 | expiration = nonPersistentSessionDuration 233 | } 234 | if expiration > 0 { 235 | expirationDate = now.Add(expiration) 236 | k, err = datastore.Put(c, k, &Session{ 237 | Date: now, 238 | ExpirationDate: expirationDate, 239 | Value: serialized, 240 | }) 241 | if err != nil { 242 | return err 243 | } 244 | } else { 245 | err = datastore.Delete(c, k) 246 | if err != nil { 247 | return err 248 | } 249 | } 250 | return nil 251 | } 252 | 253 | // load gets a value from datastore and decodes its content into 254 | // session.Values. 255 | func loadFromDatastore(c appengine.Context, kind string, 256 | session *sessions.Session) error { 257 | k := datastore.NewKey(c, kind, session.ID, 0, nil) 258 | entity := Session{} 259 | if err := datastore.Get(c, k, &entity); err != nil { 260 | return err 261 | } 262 | if err := deserialize(entity.Value, &session.Values); err != nil { 263 | return err 264 | } 265 | return nil 266 | } 267 | 268 | // remove expired sessions in the datastore. you can call this function 269 | // from a cron job. 270 | // 271 | // sample handler config in app.yaml: 272 | // handlers: 273 | // - url: /tasks/removeExpiredSessions 274 | // script: _go_app 275 | // login: admin 276 | // - url: /.* 277 | // script: _go_app 278 | // 279 | // handler registration code: 280 | // http.HandleFunc("/tasks/removeExpiredSessions", removeExpiredSessionsHandler) 281 | // 282 | // sample handler: 283 | // func removeExpiredSessionsHandler(w http.ResponseWriter, r *http.Request) { 284 | // c := appengine.NewContext(r) 285 | // gaesessions.RemoveExpiredDatastoreSessions(c, "") 286 | // } 287 | // 288 | // sample cron.yaml: 289 | // cron: 290 | // - description: expired session removal job 291 | // url: /tasks/removeExpiredSessions 292 | // schedule: every 1 minutes 293 | func RemoveExpiredDatastoreSessions(c appengine.Context, kind string) error { 294 | keys, err := findExpiredDatastoreSessionKeys(c, kind) 295 | if err != nil { 296 | return err 297 | } 298 | return datastore.DeleteMulti(c, keys) 299 | } 300 | 301 | func findExpiredDatastoreSessionKeys(c appengine.Context, kind string) (keys []*datastore.Key, err error) { 302 | if kind == "" { 303 | kind = defaultKind 304 | } 305 | now := time.Now() 306 | q := datastore.NewQuery(kind).Filter("ExpirationDate <=", now).KeysOnly() 307 | keys, err = q.GetAll(c, nil) 308 | return 309 | } 310 | 311 | // MemcacheStore -------------------------------------------------------------- 312 | 313 | // NewMemcacheStore returns a new MemcacheStore. 314 | // 315 | // The keyPrefix argument is the prefix used for memcache keys. If empty it 316 | // will use "gorilla.appengine.sessions.". 317 | // 318 | // See NewCookieStore() for a description of the other parameters. 319 | func NewMemcacheStore(keyPrefix string, nonPersistentSessionDuration time.Duration, keyPairs ...[]byte) *MemcacheStore { 320 | if keyPrefix == "" { 321 | keyPrefix = "gorilla.appengine.sessions." 322 | } 323 | return &MemcacheStore{ 324 | Codecs: securecookie.CodecsFromPairs(keyPairs...), 325 | Options: &sessions.Options{ 326 | Path: "/", 327 | MaxAge: 86400 * 30, 328 | }, 329 | prefix: keyPrefix, 330 | nonPersistentSessionDuration: nonPersistentSessionDuration, 331 | } 332 | } 333 | 334 | // MemcacheStore stores sessions in the App Engine memcache. 335 | type MemcacheStore struct { 336 | Codecs []securecookie.Codec 337 | Options *sessions.Options // default configuration 338 | prefix string 339 | nonPersistentSessionDuration time.Duration 340 | } 341 | 342 | // Get returns a session for the given name after adding it to the registry. 343 | // 344 | // See CookieStore.Get(). 345 | func (s *MemcacheStore) Get(r *http.Request, name string) (*sessions.Session, 346 | error) { 347 | return sessions.GetRegistry(r).Get(s, name) 348 | } 349 | 350 | // New returns a session for the given name without adding it to the registry. 351 | // 352 | // See CookieStore.New(). 353 | func (s *MemcacheStore) New(r *http.Request, name string) (*sessions.Session, 354 | error) { 355 | session := sessions.NewSession(s, name) 356 | session.Options = &(*s.Options) 357 | session.IsNew = true 358 | var err error 359 | if cookie, errCookie := r.Cookie(name); errCookie == nil { 360 | err = securecookie.DecodeMulti(name, cookie.Value, &session.ID, 361 | s.Codecs...) 362 | if err == nil { 363 | c := appengine.NewContext(r) 364 | err = loadFromMemcache(c, session) 365 | if err == nil { 366 | session.IsNew = false 367 | } 368 | } 369 | } 370 | return session, err 371 | } 372 | 373 | // Save adds a single session to the response. 374 | func (s *MemcacheStore) Save(r *http.Request, w http.ResponseWriter, 375 | session *sessions.Session) error { 376 | if session.ID == "" { 377 | session.ID = s.prefix + 378 | strings.TrimRight( 379 | base32.StdEncoding.EncodeToString( 380 | securecookie.GenerateRandomKey(32)), "=") 381 | } 382 | c := appengine.NewContext(r) 383 | if err := saveToMemcache(c, s.nonPersistentSessionDuration, session); err != nil { 384 | return err 385 | } 386 | encoded, err := securecookie.EncodeMulti(session.Name(), session.ID, 387 | s.Codecs...) 388 | if err != nil { 389 | return err 390 | } 391 | http.SetCookie(w, sessions.NewCookie(session.Name(), encoded, 392 | session.Options)) 393 | return nil 394 | } 395 | 396 | // save writes encoded session.Values to memcache. 397 | func saveToMemcache(c appengine.Context, 398 | nonPersistentSessionDuration time.Duration, 399 | session *sessions.Session) error { 400 | if len(session.Values) == 0 { 401 | // Don't need to write anything. 402 | return nil 403 | } 404 | serialized, err := serialize(session.Values) 405 | if err != nil { 406 | return err 407 | } 408 | var expiration time.Duration 409 | if session.Options.MaxAge > 0 { 410 | expiration = time.Duration(session.Options.MaxAge) * time.Second 411 | } else { 412 | expiration = nonPersistentSessionDuration 413 | } 414 | if expiration > 0 { 415 | c.Debugf("MemcacheStore.save. session.ID=%s, expiration=%s", 416 | session.ID, expiration) 417 | err = memcache.Set(c, &memcache.Item{ 418 | Key: session.ID, 419 | Value: serialized, 420 | Expiration: expiration, 421 | }) 422 | if err != nil { 423 | return err 424 | } 425 | } else { 426 | err = memcache.Delete(c, session.ID) 427 | if err != nil { 428 | return err 429 | } 430 | c.Debugf("MemcacheStore.save. delete session.ID=%s", session.ID) 431 | } 432 | return nil 433 | } 434 | 435 | // load gets a value from memcache and decodes its content into session.Values. 436 | func loadFromMemcache(c appengine.Context, session *sessions.Session) error { 437 | item, err := memcache.Get(c, session.ID) 438 | if err != nil { 439 | return err 440 | } 441 | if err := deserialize(item.Value, &session.Values); err != nil { 442 | return err 443 | } 444 | return nil 445 | } 446 | 447 | // Serialization -------------------------------------------------------------- 448 | 449 | // serialize encodes a value using gob. 450 | func serialize(src interface{}) ([]byte, error) { 451 | buf := new(bytes.Buffer) 452 | enc := gob.NewEncoder(buf) 453 | if err := enc.Encode(src); err != nil { 454 | return nil, err 455 | } 456 | return buf.Bytes(), nil 457 | } 458 | 459 | // deserialize decodes a value using gob. 460 | func deserialize(src []byte, dst interface{}) error { 461 | dec := gob.NewDecoder(bytes.NewBuffer(src)) 462 | if err := dec.Decode(dst); err != nil { 463 | return err 464 | } 465 | return nil 466 | } 467 | --------------------------------------------------------------------------------