1
0
mirror of https://github.com/kataras/iris.git synced 2026-01-23 20:05:59 +00:00

Publish the new version ✈️ | Look description please!

# FAQ

### Looking for free support?

	http://support.iris-go.com
    https://kataras.rocket.chat/channel/iris

### Looking for previous versions?

    https://github.com/kataras/iris#version

### Should I upgrade my Iris?

Developers are not forced to upgrade if they don't really need it. Upgrade whenever you feel ready.
> Iris uses the [vendor directory](https://docs.google.com/document/d/1Bz5-UB7g2uPBdOx-rw5t9MxJwkfpx90cqG9AFL0JAYo) feature, so you get truly reproducible builds, as this method guards against upstream renames and deletes.

**How to upgrade**: Open your command-line and execute this command: `go get -u github.com/kataras/iris`.
For further installation support, please click [here](http://support.iris-go.com/d/16-how-to-install-iris-web-framework).

### About our new home page
    http://iris-go.com

Thanks to [Santosh Anand](https://github.com/santoshanand) the http://iris-go.com has been upgraded and it's really awesome!

[Santosh](https://github.com/santoshanand) is a freelancer, he has a great knowledge of nodejs and express js, Android, iOS, React Native, Vue.js etc, if you need a developer to find or create a solution for your problem or task, please contact with him.

The amount of the next two or three donations you'll send they will be immediately transferred to his own account balance, so be generous please!

Read more at https://github.com/kataras/iris/blob/master/HISTORY.md


Former-commit-id: eec2d71bbe011d6b48d2526eb25919e36e5ad94e
This commit is contained in:
kataras
2017-06-03 23:22:52 +03:00
parent 03bcadadec
commit 5e4b63acb2
330 changed files with 35786 additions and 17316 deletions

27
sessions/LICENSE Normal file
View File

@@ -0,0 +1,27 @@
Copyright (c) 2017 Gerasimos Maropoulos, ΓΜ. 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 Gerasimos Maropoulos nor the name of his
username, kataras, 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.

98
sessions/config.go Normal file
View File

@@ -0,0 +1,98 @@
// Copyright 2017 Gerasimos Maropoulos, ΓΜ. 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 (
"time"
"github.com/satori/go.uuid"
)
const (
// DefaultCookieName the secret cookie's name for sessions
DefaultCookieName = "irissessionid"
// DefaultCookieLength is the default Session Manager's CookieLength, which is 32
DefaultCookieLength = 32
)
type (
// Config is the configuration for sessions. Please review it well before using sessions.
Config struct {
// Cookie string, the session's client cookie name, for example: "mysessionid"
//
// Defaults to "irissessionid"
Cookie string
// CookieSecureTLS set to true if server is running over TLS
// and you need the session's cookie "Secure" field to be setted true.
//
// Note: The user should fill the Decode configuation field in order for this to work.
// Recommendation: You don't need this to be setted to true, just fill the Encode and Decode fields
// with a third-party library like secure cookie, example is provided at the _examples folder.
//
// Defaults to false
CookieSecureTLS bool
// Encode the cookie value if not nil.
// Should accept as first argument the cookie name (config.Name)
// as second argument the server's generated session id.
// Should return the new session id, if error the session id setted to empty which is invalid.
//
// Note: Errors are not printed, so you have to know what you're doing,
// and remember: if you use AES it only supports key sizes of 16, 24 or 32 bytes.
// You either need to provide exactly that amount or you derive the key from what you type in.
//
// Defaults to nil
Encode func(cookieName string, value interface{}) (string, error)
// Decode the cookie value if not nil.
// Should accept as first argument the cookie name (config.Name)
// as second second accepts the client's cookie value (the encoded session id).
// Should return an error if decode operation failed.
//
// Note: Errors are not printed, so you have to know what you're doing,
// and remember: if you use AES it only supports key sizes of 16, 24 or 32 bytes.
// You either need to provide exactly that amount or you derive the key from what you type in.
//
// Defaults to nil
Decode func(cookieName string, cookieValue string, v interface{}) error
// Expires the duration of which the cookie must expires (created_time.Add(Expires)).
// If you want to delete the cookie when the browser closes, set it to -1.
//
// 0 means no expire, (24 years)
// -1 means when browser closes
// > 0 is the time.Duration which the session cookies should expire.
//
// Defaults to infinitive/unlimited life duration(0)
Expires time.Duration
// SessionIDGenerator should returns a random session id.
// By default we will use a uuid impl package to generate
// that, but developers can change that with simple assignment.
SessionIDGenerator func() string
// DisableSubdomainPersistence set it to true in order dissallow your q subdomains to have access to the session cookie
//
// Defaults to false
DisableSubdomainPersistence bool
}
)
// Validate corrects missing fields configuration fields and returns the right configuration
func (c Config) Validate() Config {
if c.Cookie == "" {
c.Cookie = DefaultCookieName
}
if c.SessionIDGenerator == nil {
c.SessionIDGenerator = func() string {
id := uuid.NewV4()
return id.String()
}
}
return c
}

82
sessions/cookie.go Normal file
View File

@@ -0,0 +1,82 @@
// Copyright 2017 Gerasimos Maropoulos, ΓΜ. 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 (
"net/http"
"strconv"
"strings"
"time"
)
var (
// CookieExpireDelete may be set on Cookie.Expire for expiring the given cookie.
CookieExpireDelete = time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC)
// CookieExpireUnlimited indicates that the cookie doesn't expire.
CookieExpireUnlimited = time.Now().AddDate(24, 10, 10)
)
// GetCookie returns cookie's value by it's name
// returns empty string if nothing was found
func GetCookie(name string, req *http.Request) string {
c, err := req.Cookie(name)
if err != nil {
return ""
}
return c.Value
}
// AddCookie adds a cookie
func AddCookie(cookie *http.Cookie, res http.ResponseWriter) {
if v := cookie.String(); v != "" {
http.SetCookie(res, cookie)
}
}
// RemoveCookie deletes a cookie by it's name/key
func RemoveCookie(name string, res http.ResponseWriter, req *http.Request) {
c, err := req.Cookie(name)
if err != nil {
return
}
c.Expires = CookieExpireDelete
// MaxAge<0 means delete cookie now, equivalently 'Max-Age: 0'
c.MaxAge = -1
c.Value = ""
c.Path = "/"
AddCookie(c, res)
}
// IsValidCookieDomain returns true if the receiver is a valid domain to set
// valid means that is recognised as 'domain' by the browser, so it(the cookie) can be shared with subdomains also
func IsValidCookieDomain(domain string) bool {
if domain == "0.0.0.0" || domain == "127.0.0.1" {
// for these type of hosts, we can't allow subdomains persistence,
// the web browser doesn't understand the mysubdomain.0.0.0.0 and mysubdomain.127.0.0.1 mysubdomain.32.196.56.181. as scorrectly ubdomains because of the many dots
// so don't set a cookie domain here, let browser handle this
return false
}
dotLen := strings.Count(domain, ".")
if dotLen == 0 {
// we don't have a domain, maybe something like 'localhost', browser doesn't see the .localhost as wildcard subdomain+domain
return false
}
if dotLen >= 3 {
if lastDotIdx := strings.LastIndexByte(domain, '.'); lastDotIdx != -1 {
// chekc the last part, if it's number then propably it's ip
if len(domain) > lastDotIdx+1 {
_, err := strconv.Atoi(domain[lastDotIdx+1:])
if err == nil {
return false
}
}
}
}
return true
}

21
sessions/database.go Normal file
View File

@@ -0,0 +1,21 @@
// Copyright 2017 Gerasimos Maropoulos, ΓΜ. 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
// Database is the interface which all session databases should implement
// By design it doesn't support any type of cookie session like other frameworks,
// I want to protect you, believe me, no context access (although we could)
// The scope of the database is to session somewhere the sessions in order to
// keep them after restarting the server, nothing more.
// the values are sessiond by the underline session, the check for new sessions, or
// 'this session value should added' are made automatically by q, you are able just to set the values to your backend database with Load function.
// session database doesn't have any write or read access to the session, the loading of
// the initial data is done by the Load(string) map[string]interfface{} function
// synchronization are made automatically, you can register more than one session database
// but the first non-empty Load return data will be used as the session values.
type Database interface {
Load(string) map[string]interface{}
Update(string, map[string]interface{})
}

128
sessions/provider.go Normal file
View File

@@ -0,0 +1,128 @@
// Copyright 2017 Gerasimos Maropoulos, ΓΜ. 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 (
"sync"
"time"
)
type (
// provider contains the sessions and external databases (load and update).
// It's the session memory manager
provider struct {
// we don't use RWMutex because all actions have read and write at the same action function.
// (or write to a *session's value which is race if we don't lock)
// narrow locks are fasters but are useless here.
mu sync.Mutex
sessions map[string]*session
databases []Database
}
)
// newProvider returns a new sessions provider
func newProvider() *provider {
return &provider{
sessions: make(map[string]*session, 0),
databases: make([]Database, 0),
}
}
// RegisterDatabase adds a session database
// a session db doesn't have write access
func (p *provider) RegisterDatabase(db Database) {
p.mu.Lock() // for any case
p.databases = append(p.databases, db)
p.mu.Unlock()
}
// newSession returns a new session from sessionid
func (p *provider) newSession(sid string, expires time.Duration) *session {
sess := &session{
sid: sid,
provider: p,
values: p.loadSessionValues(sid),
flashes: make(map[string]*flashMessage),
}
if expires > 0 { // if not unlimited life duration and no -1 (cookie remove action is based on browser's session)
time.AfterFunc(expires, func() {
// the destroy makes the check if this session is exists then or not,
// this is used to destroy the session from the server-side also
// it's good to have here for security reasons, I didn't add it on the gc function to separate its action
p.Destroy(sid)
})
}
return sess
}
func (p *provider) loadSessionValues(sid string) map[string]interface{} {
for i, n := 0, len(p.databases); i < n; i++ {
if dbValues := p.databases[i].Load(sid); dbValues != nil && len(dbValues) > 0 {
return dbValues // return the first non-empty from the registered stores.
}
}
values := make(map[string]interface{})
return values
}
func (p *provider) updateDatabases(sid string, newValues map[string]interface{}) {
for i, n := 0, len(p.databases); i < n; i++ {
p.databases[i].Update(sid, newValues)
}
}
// Init creates the session and returns it
func (p *provider) Init(sid string, expires time.Duration) Session {
newSession := p.newSession(sid, expires)
p.mu.Lock()
p.sessions[sid] = newSession
p.mu.Unlock()
return newSession
}
// Read returns the store which sid parameter belongs
func (p *provider) Read(sid string, expires time.Duration) Session {
p.mu.Lock()
if sess, found := p.sessions[sid]; found {
sess.runFlashGC() // run the flash messages GC, new request here of existing session
p.mu.Unlock()
return sess
}
p.mu.Unlock()
return p.Init(sid, expires) // if not found create new
}
// Destroy destroys the session, removes all sessions and flash values,
// the session itself and updates the registered session databases,
// this called from sessionManager which removes the client's cookie also.
func (p *provider) Destroy(sid string) {
p.mu.Lock()
if sess, found := p.sessions[sid]; found {
sess.values = nil
sess.flashes = nil
delete(p.sessions, sid)
p.updateDatabases(sid, nil)
}
p.mu.Unlock()
}
// DestroyAll removes all sessions
// from the server-side memory (and database if registered).
// Client's session cookie will still exist but it will be reseted on the next request.
func (p *provider) DestroyAll() {
p.mu.Lock()
for _, sess := range p.sessions {
delete(p.sessions, sess.ID())
p.updateDatabases(sess.ID(), nil)
}
p.mu.Unlock()
}

319
sessions/session.go Normal file
View File

@@ -0,0 +1,319 @@
// Copyright 2017 Gerasimos Maropoulos, ΓΜ. 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 (
"strconv"
"sync"
"time"
"github.com/kataras/iris/core/errors"
)
type (
// session is an 'object' which wraps the session provider with its session databases, only frontend user has access to this session object.
// implements the context.Session interface
session struct {
sid string
values map[string]interface{} // here are the real values
// we could set the flash messages inside values but this will bring us more problems
// because of session databases and because of
// users may want to get all sessions and save them or display them
// but without temp values (flash messages) which are removed after fetching.
// so introduce a new field here.
// NOTE: flashes are not managed by third-party, only inside session struct.
flashes map[string]*flashMessage
mu sync.RWMutex
createdAt time.Time
provider *provider
}
flashMessage struct {
// if true then this flash message is removed on the flash gc
shouldRemove bool
value interface{}
}
)
var _ Session = &session{}
// ID returns the session's id
func (s *session) ID() string {
return s.sid
}
// Get returns the value of an entry by its key
func (s *session) Get(key string) interface{} {
s.mu.RLock()
value := s.values[key]
s.mu.RUnlock()
return value
}
// when running on the session manager removes any 'old' flash messages
func (s *session) runFlashGC() {
s.mu.Lock()
for key, v := range s.flashes {
if v.shouldRemove {
delete(s.flashes, key)
}
}
s.mu.Unlock()
}
// HasFlash returns true if this request has available flash messages
func (s *session) HasFlash() bool {
return s.flashes != nil && len(s.flashes) > 0
}
// GetFlash returns a flash message which removed on the next request
//
// To check for flash messages we use the HasFlash() Method
// and to obtain the flash message we use the GetFlash() Method.
// There is also a method GetFlashes() to fetch all the messages.
//
// Fetching a message deletes it from the session.
// This means that a message is meant to be displayed only on the first page served to the user
func (s *session) GetFlash(key string) (v interface{}) {
s.mu.Lock()
if valueStorage, found := s.flashes[key]; found {
valueStorage.shouldRemove = true
v = valueStorage.value
}
s.mu.Unlock()
return
}
// GetString same as Get but returns as string, if nil then returns an empty string
func (s *session) GetString(key string) string {
if value := s.Get(key); value != nil {
if v, ok := value.(string); ok {
return v
}
}
return ""
}
// GetFlashString same as GetFlash but returns as string, if nil then returns an empty string
func (s *session) GetFlashString(key string) string {
if value := s.GetFlash(key); value != nil {
if v, ok := value.(string); ok {
return v
}
}
return ""
}
var errFindParse = errors.New("Unable to find the %s with key: %s. Found? %#v")
// GetInt same as Get but returns as int, if not found then returns -1 and an error
func (s *session) GetInt(key string) (int, error) {
v := s.Get(key)
if vint, ok := v.(int); ok {
return vint, nil
}
if vstring, sok := v.(string); sok {
return strconv.Atoi(vstring)
}
return -1, errFindParse.Format("int", key, v)
}
// GetInt64 same as Get but returns as int64, if not found then returns -1 and an error
func (s *session) GetInt64(key string) (int64, error) {
v := s.Get(key)
if vint64, ok := v.(int64); ok {
return vint64, nil
}
if vint, ok := v.(int); ok {
return int64(vint), nil
}
if vstring, sok := v.(string); sok {
return strconv.ParseInt(vstring, 10, 64)
}
return -1, errFindParse.Format("int64", key, v)
}
// GetFloat32 same as Get but returns as float32, if not found then returns -1 and an error
func (s *session) GetFloat32(key string) (float32, error) {
v := s.Get(key)
if vfloat32, ok := v.(float32); ok {
return vfloat32, nil
}
if vfloat64, ok := v.(float64); ok {
return float32(vfloat64), nil
}
if vint, ok := v.(int); ok {
return float32(vint), nil
}
if vstring, sok := v.(string); sok {
vfloat64, err := strconv.ParseFloat(vstring, 32)
if err != nil {
return -1, err
}
return float32(vfloat64), nil
}
return -1, errFindParse.Format("float32", key, v)
}
// GetFloat64 same as Get but returns as float64, if not found then returns -1 and an error
func (s *session) GetFloat64(key string) (float64, error) {
v := s.Get(key)
if vfloat32, ok := v.(float32); ok {
return float64(vfloat32), nil
}
if vfloat64, ok := v.(float64); ok {
return vfloat64, nil
}
if vint, ok := v.(int); ok {
return float64(vint), nil
}
if vstring, sok := v.(string); sok {
return strconv.ParseFloat(vstring, 32)
}
return -1, errFindParse.Format("float64", key, v)
}
// GetBoolean same as Get but returns as boolean, if not found then returns -1 and an error
func (s *session) GetBoolean(key string) (bool, error) {
v := s.Get(key)
// here we could check for "true", "false" and 0 for false and 1 for true
// but this may cause unexpected behavior from the developer if they expecting an error
// so we just check if bool, if yes then return that bool, otherwise return false and an error
if vb, ok := v.(bool); ok {
return vb, nil
}
return false, errFindParse.Format("bool", key, v)
}
// GetAll returns a copy of all session's values
func (s *session) GetAll() map[string]interface{} {
items := make(map[string]interface{}, len(s.values))
s.mu.RLock()
for key, v := range s.values {
items[key] = v
}
s.mu.RUnlock()
return items
}
// GetFlashes returns all flash messages as map[string](key) and interface{} value
// NOTE: this will cause at remove all current flash messages on the next request of the same user
func (s *session) GetFlashes() map[string]interface{} {
flashes := make(map[string]interface{}, len(s.flashes))
s.mu.Lock()
for key, v := range s.flashes {
flashes[key] = v.value
v.shouldRemove = true
}
s.mu.Unlock()
return flashes
}
// VisitAll loop each one entry and calls the callback function func(key,value)
func (s *session) VisitAll(cb func(k string, v interface{})) {
for key := range s.values {
cb(key, s.values[key])
}
}
// Set fills the session with an entry, it receives a key and a value
// returns an error, which is always nil
func (s *session) Set(key string, value interface{}) {
s.mu.Lock()
s.values[key] = value
s.mu.Unlock()
s.updateDatabases()
}
// SetFlash sets a flash message by its key.
//
// A flash message is used in order to keep a message in session through one or several requests of the same user.
// It is removed from session after it has been displayed to the user.
// Flash messages are usually used in combination with HTTP redirections,
// because in this case there is no view, so messages can only be displayed in the request that follows redirection.
//
// A flash message has a name and a content (AKA key and value).
// It is an entry of an associative array. The name is a string: often "notice", "success", or "error", but it can be anything.
// The content is usually a string. You can put HTML tags in your message if you display it raw.
// You can also set the message value to a number or an array: it will be serialized and kept in session like a string.
//
// Flash messages can be set using the SetFlash() Method
// For example, if you would like to inform the user that his changes were successfully saved,
// you could add the following line to your Handler:
//
// SetFlash("success", "Data saved!");
//
// In this example we used the key 'success'.
// If you want to define more than one flash messages, you will have to use different keys
func (s *session) SetFlash(key string, value interface{}) {
s.mu.Lock()
s.flashes[key] = &flashMessage{value: value}
s.mu.Unlock()
}
// Delete removes an entry by its key
func (s *session) Delete(key string) {
s.mu.Lock()
delete(s.values, key)
s.mu.Unlock()
s.updateDatabases()
}
func (s *session) updateDatabases() {
s.provider.updateDatabases(s.sid, s.values)
}
// DeleteFlash removes a flash message by its key
func (s *session) DeleteFlash(key string) {
s.mu.Lock()
delete(s.flashes, key)
s.mu.Unlock()
}
// Clear removes all entries
func (s *session) Clear() {
s.mu.Lock()
for key := range s.values {
delete(s.values, key)
}
s.mu.Unlock()
s.updateDatabases()
}
// Clear removes all flash messages
func (s *session) ClearFlashes() {
s.mu.Lock()
for key := range s.flashes {
delete(s.flashes, key)
}
s.mu.Unlock()
}

View File

@@ -0,0 +1,29 @@
## Session databases
Find more databases at [github.com/kataras/go-sessions/sessiondb](https://github.com/kataras/go-sessions/tree/master/sessiondb).
This folder contains only the redis database because the rest (two so far, 'file' and 'leveldb') were created by the Community.
So go [there](https://github.com/kataras/go-sessions/tree/master/sessiondb) and find more about them. `Database` is just an
interface so you're able to `UseDatabase(anyCompatibleDatabase)`. A Database should implement two functions, `Load` and `Update`.
**Database interface**
```go
type Database interface {
Load(string) map[string]interface{}
Update(string, map[string]interface{})
}
```
```go
import (
"...myDatabase"
)
s := New(...)
s.UseDatabase(myDatabase) // <---
app := iris.New()
app.Adapt(s)
app.Listen(":8080")
```

View File

@@ -0,0 +1,91 @@
// Copyright 2017 Gerasimos Maropoulos. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package redis
import (
"bytes"
"encoding/gob"
"github.com/kataras/iris/sessions/sessiondb/redis/service"
)
// Database the redis database for q sessions
type Database struct {
redis *service.Service
}
// New returns a new redis database
func New(cfg ...service.Config) *Database {
return &Database{redis: service.New(cfg...)}
}
// Config returns the configuration for the redis server bridge, you can change them
func (d *Database) Config() *service.Config {
return d.redis.Config
}
// Load loads the values to the underline
func (d *Database) Load(sid string) map[string]interface{} {
values := make(map[string]interface{})
if !d.redis.Connected { //yes, check every first time's session for valid redis connection
d.redis.Connect()
_, err := d.redis.PingPong()
if err != nil {
if err != nil {
// don't use to get the logger, just prin these to the console... atm
///TODO: Find a way to use the iris' defined logger via an optional interface to Database.
// println("Redis Connection error on Connect: " + err.Error())
// println("But don't panic, auto-switching to memory store right now!")
}
}
}
//fetch the values from this session id and copy-> store them
val, err := d.redis.GetBytes(sid)
if err == nil {
// err removed because of previous TODO
DeserializeBytes(val, &values)
}
return values
}
// serialize the values to be stored as strings inside the Redis, we panic at any serialization error here
func serialize(values map[string]interface{}) []byte {
val, err := SerializeBytes(values)
if err != nil {
println("On redisstore.serialize: " + err.Error())
}
return val
}
// Update updates the real redis store
func (d *Database) Update(sid string, newValues map[string]interface{}) {
if len(newValues) == 0 {
go d.redis.Delete(sid)
} else {
go d.redis.Set(sid, serialize(newValues)) //set/update all the values
}
}
// SerializeBytes serializa bytes using gob encoder and returns them
func SerializeBytes(m interface{}) ([]byte, error) {
buf := new(bytes.Buffer)
enc := gob.NewEncoder(buf)
err := enc.Encode(m)
if err == nil {
return buf.Bytes(), nil
}
return nil, err
}
// DeserializeBytes converts the bytes to an object using gob decoder
func DeserializeBytes(b []byte, m interface{}) error {
dec := gob.NewDecoder(bytes.NewBuffer(b))
return dec.Decode(m) //no reference here otherwise doesn't work because of go remote object
}

View File

@@ -0,0 +1,82 @@
// Copyright 2017 Gerasimos Maropoulos. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package service
import (
"time"
"github.com/imdario/mergo"
)
const (
// DefaultRedisNetwork the redis network option, "tcp"
DefaultRedisNetwork = "tcp"
// DefaultRedisAddr the redis address option, "127.0.0.1:6379"
DefaultRedisAddr = "127.0.0.1:6379"
// DefaultRedisIdleTimeout the redis idle timeout option, time.Duration(5) * time.Minute
DefaultRedisIdleTimeout = time.Duration(5) * time.Minute
// DefaultRedisMaxAgeSeconds the redis storage last parameter (SETEX), 31556926.0 (1 year)
DefaultRedisMaxAgeSeconds = 31556926.0 //1 year
)
// Config the redis configuration used inside sessions
type Config struct {
// Network "tcp"
Network string
// Addr "127.0.0.1:6379"
Addr string
// Password string .If no password then no 'AUTH'. Default ""
Password string
// If Database is empty "" then no 'SELECT'. Default ""
Database string
// MaxIdle 0 no limit
MaxIdle int
// MaxActive 0 no limit
MaxActive int
// IdleTimeout time.Duration(5) * time.Minute
IdleTimeout time.Duration
// Prefix "myprefix-for-this-website". Default ""
Prefix string
// MaxAgeSeconds how much long the redis should keep the session in seconds. Default 31556926.0 (1 year)
MaxAgeSeconds int
}
// DefaultConfig returns the default configuration for Redis service
func DefaultConfig() Config {
return Config{
Network: DefaultRedisNetwork,
Addr: DefaultRedisAddr,
Password: "",
Database: "",
MaxIdle: 0,
MaxActive: 0,
IdleTimeout: DefaultRedisIdleTimeout,
Prefix: "",
MaxAgeSeconds: DefaultRedisMaxAgeSeconds,
}
}
// Merge merges the default with the given config and returns the result
func (c Config) Merge(cfg []Config) (config Config) {
if len(cfg) > 0 {
config = cfg[0]
mergo.Merge(&config, c)
} else {
_default := c
config = _default
}
return
}
// MergeSingle merges the default with the given config and returns the result
func (c Config) MergeSingle(cfg Config) (config Config) {
config = cfg
mergo.Merge(&config, c)
return
}

View File

@@ -0,0 +1,276 @@
// Copyright 2017 Gerasimos Maropoulos. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package service
import (
"time"
"github.com/garyburd/redigo/redis"
"github.com/kataras/iris/core/errors"
)
var (
// ErrRedisClosed an error with message 'Redis is already closed'
ErrRedisClosed = errors.New("Redis is already closed")
// ErrKeyNotFound an error with message 'Key $thekey doesn't found'
ErrKeyNotFound = errors.New("Key '%s' doesn't found")
)
// Service the Redis service, contains the config and the redis pool
type Service struct {
// Connected is true when the Service has already connected
Connected bool
// Config the redis config for this redis
Config *Config
pool *redis.Pool
}
// PingPong sends a ping and receives a pong, if no pong received then returns false and filled error
func (r *Service) PingPong() (bool, error) {
c := r.pool.Get()
defer c.Close()
msg, err := c.Do("PING")
if err != nil || msg == nil {
return false, err
}
return (msg == "PONG"), nil
}
// CloseConnection closes the redis connection
func (r *Service) CloseConnection() error {
if r.pool != nil {
return r.pool.Close()
}
return ErrRedisClosed
}
// Set sets to the redis
// key string, value string, you can use utils.Serialize(&myobject{}) to convert an object to []byte
func (r *Service) Set(key string, value []byte) (err error) { // map[interface{}]interface{}) (err error) {
c := r.pool.Get()
defer c.Close()
if err = c.Err(); err != nil {
return
}
_, err = c.Do("SETEX", r.Config.Prefix+key, r.Config.MaxAgeSeconds, value)
return
}
// Get returns value, err by its key
// you can use utils.Deserialize((.Get("yourkey"),&theobject{})
//returns nil and a filled error if something wrong happens
func (r *Service) Get(key string) (interface{}, error) {
c := r.pool.Get()
defer c.Close()
if err := c.Err(); err != nil {
return nil, err
}
redisVal, err := c.Do("GET", r.Config.Prefix+key)
if err != nil {
return nil, err
}
if redisVal == nil {
return nil, ErrKeyNotFound.Format(key)
}
return redisVal, nil
}
// GetBytes returns value, err by its key
// you can use utils.Deserialize((.GetBytes("yourkey"),&theobject{})
//returns nil and a filled error if something wrong happens
func (r *Service) GetBytes(key string) ([]byte, error) {
c := r.pool.Get()
defer c.Close()
if err := c.Err(); err != nil {
return nil, err
}
redisVal, err := c.Do("GET", r.Config.Prefix+key)
if err != nil {
return nil, err
}
if redisVal == nil {
return nil, ErrKeyNotFound.Format(key)
}
return redis.Bytes(redisVal, err)
}
// GetString returns value, err by its key
// you can use utils.Deserialize((.GetString("yourkey"),&theobject{})
//returns empty string and a filled error if something wrong happens
func (r *Service) GetString(key string) (string, error) {
redisVal, err := r.Get(key)
if redisVal == nil {
return "", ErrKeyNotFound.Format(key)
}
sVal, err := redis.String(redisVal, err)
if err != nil {
return "", err
}
return sVal, nil
}
// GetInt returns value, err by its key
// you can use utils.Deserialize((.GetInt("yourkey"),&theobject{})
//returns -1 int and a filled error if something wrong happens
func (r *Service) GetInt(key string) (int, error) {
redisVal, err := r.Get(key)
if redisVal == nil {
return -1, ErrKeyNotFound.Format(key)
}
intVal, err := redis.Int(redisVal, err)
if err != nil {
return -1, err
}
return intVal, nil
}
// GetStringMap returns map[string]string, err by its key
//returns nil and a filled error if something wrong happens
func (r *Service) GetStringMap(key string) (map[string]string, error) {
redisVal, err := r.Get(key)
if redisVal == nil {
return nil, ErrKeyNotFound.Format(key)
}
_map, err := redis.StringMap(redisVal, err)
if err != nil {
return nil, err
}
return _map, nil
}
// GetAll returns all keys and their values from a specific key (map[string]string)
// returns a filled error if something bad happened
func (r *Service) GetAll(key string) (map[string]string, error) {
c := r.pool.Get()
defer c.Close()
if err := c.Err(); err != nil {
return nil, err
}
reply, err := c.Do("HGETALL", r.Config.Prefix+key)
if err != nil {
return nil, err
}
if reply == nil {
return nil, ErrKeyNotFound.Format(key)
}
return redis.StringMap(reply, err)
}
// GetAllKeysByPrefix returns all []string keys by a key prefix from the redis
func (r *Service) GetAllKeysByPrefix(prefix string) ([]string, error) {
c := r.pool.Get()
defer c.Close()
if err := c.Err(); err != nil {
return nil, err
}
reply, err := c.Do("KEYS", r.Config.Prefix+prefix)
if err != nil {
return nil, err
}
if reply == nil {
return nil, ErrKeyNotFound.Format(prefix)
}
return redis.Strings(reply, err)
}
// Delete removes redis entry by specific key
func (r *Service) Delete(key string) error {
c := r.pool.Get()
defer c.Close()
if _, err := c.Do("DEL", r.Config.Prefix+key); err != nil {
return err
}
return nil
}
func dial(network string, addr string, pass string) (redis.Conn, error) {
if network == "" {
network = DefaultRedisNetwork
}
if addr == "" {
addr = DefaultRedisAddr
}
c, err := redis.Dial(network, addr)
if err != nil {
return nil, err
}
if pass != "" {
if _, err = c.Do("AUTH", pass); err != nil {
c.Close()
return nil, err
}
}
return c, err
}
// Connect connects to the redis, called only once
func (r *Service) Connect() {
c := r.Config
if c.IdleTimeout <= 0 {
c.IdleTimeout = DefaultRedisIdleTimeout
}
if c.Network == "" {
c.Network = DefaultRedisNetwork
}
if c.Addr == "" {
c.Addr = DefaultRedisAddr
}
if c.MaxAgeSeconds <= 0 {
c.MaxAgeSeconds = DefaultRedisMaxAgeSeconds
}
pool := &redis.Pool{IdleTimeout: DefaultRedisIdleTimeout, MaxIdle: c.MaxIdle, MaxActive: c.MaxActive}
pool.TestOnBorrow = func(c redis.Conn, t time.Time) error {
_, err := c.Do("PING")
return err
}
if c.Database != "" {
pool.Dial = func() (redis.Conn, error) {
red, err := dial(c.Network, c.Addr, c.Password)
if err != nil {
return nil, err
}
if _, err = red.Do("SELECT", c.Database); err != nil {
red.Close()
return nil, err
}
return red, err
}
} else {
pool.Dial = func() (redis.Conn, error) {
return dial(c.Network, c.Addr, c.Password)
}
}
r.Connected = true
r.pool = pool
}
// New returns a Redis service filled by the passed config
// to connect call the .Connect()
func New(cfg ...Config) *Service {
c := DefaultConfig().Merge(cfg)
r := &Service{pool: &redis.Pool{}, Config: &c}
return r
}

219
sessions/sessions.go Normal file
View File

@@ -0,0 +1,219 @@
// Copyright 2017 Gerasimos Maropoulos, ΓΜ. 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 (
"net/http"
"strings"
"time"
)
type (
// Sessions must be implemented within a session manager.
//
// A Sessions should be responsible to Start a sesion based
// on raw http.ResponseWriter and http.Request, which should return
// a compatible Session interface, type. If the external session manager
// doesn't qualifies, then the user should code the rest of the functions with empty implementation.
//
// Sessions should be responsible to Destroy a session based
// on the http.ResponseWriter and http.Request, this function should works individually.
Sessions interface {
// Start should start the session for the particular net/http request.
Start(http.ResponseWriter, *http.Request) Session
// Destroy should kills the net/http session and remove the associated cookie.
Destroy(http.ResponseWriter, *http.Request)
} // Sessions is being implemented by Manager
// Session should expose the Sessions's end-user API.
// This will be returned at the sess := context.Session().
Session interface {
ID() string
Get(string) interface{}
HasFlash() bool
GetFlash(string) interface{}
GetString(key string) string
GetFlashString(string) string
GetInt(key string) (int, error)
GetInt64(key string) (int64, error)
GetFloat32(key string) (float32, error)
GetFloat64(key string) (float64, error)
GetBoolean(key string) (bool, error)
GetAll() map[string]interface{}
GetFlashes() map[string]interface{}
VisitAll(cb func(k string, v interface{}))
Set(string, interface{})
SetFlash(string, interface{})
Delete(string)
DeleteFlash(string)
Clear()
ClearFlashes()
} // Session is being implemented inside session.go
// Manager implements the Sessions interface which Iris uses to start and destroy a session from its Context.
Manager struct {
config Config
provider *provider
}
)
// New returns a new fast, feature-rich sessions manager
// it can be adapted to an Iris station
func New(cfg Config) *Manager {
return &Manager{
config: cfg.Validate(),
provider: newProvider(),
}
}
var _ Sessions = &Manager{}
// UseDatabase adds a session database to the manager's provider,
// a session db doesn't have write access
func (s *Manager) UseDatabase(db Database) {
s.provider.RegisterDatabase(db)
}
// Start starts the session for the particular net/http request
func (s *Manager) Start(res http.ResponseWriter, req *http.Request) Session {
var sess Session
cookieValue := GetCookie(s.config.Cookie, req)
if cookieValue == "" { // cookie doesn't exists, let's generate a session and add set a cookie
sid := s.config.SessionIDGenerator()
sess = s.provider.Init(sid, s.config.Expires)
cookie := &http.Cookie{}
// The RFC makes no mention of encoding url value, so here I think to encode both sessionid key and the value using the safe(to put and to use as cookie) url-encoding
cookie.Name = s.config.Cookie
cookie.Value = sid
cookie.Path = "/"
if !s.config.DisableSubdomainPersistence {
requestDomain := req.URL.Host
if portIdx := strings.IndexByte(requestDomain, ':'); portIdx > 0 {
requestDomain = requestDomain[0:portIdx]
}
if IsValidCookieDomain(requestDomain) {
// RFC2109, we allow level 1 subdomains, but no further
// if we have localhost.com , we want the localhost.cos.
// so if we have something like: mysubdomain.localhost.com we want the localhost here
// if we have mysubsubdomain.mysubdomain.localhost.com we want the .mysubdomain.localhost.com here
// slow things here, especially the 'replace' but this is a good and understable( I hope) way to get the be able to set cookies from subdomains & domain with 1-level limit
if dotIdx := strings.LastIndexByte(requestDomain, '.'); dotIdx > 0 {
// is mysubdomain.localhost.com || mysubsubdomain.mysubdomain.localhost.com
s := requestDomain[0:dotIdx] // set mysubdomain.localhost || mysubsubdomain.mysubdomain.localhost
if secondDotIdx := strings.LastIndexByte(s, '.'); secondDotIdx > 0 {
//is mysubdomain.localhost || mysubsubdomain.mysubdomain.localhost
s = s[secondDotIdx+1:] // set to localhost || mysubdomain.localhost
}
// replace the s with the requestDomain before the domain's siffux
subdomainSuff := strings.LastIndexByte(requestDomain, '.')
if subdomainSuff > len(s) { // if it is actual exists as subdomain suffix
requestDomain = strings.Replace(requestDomain, requestDomain[0:subdomainSuff], s, 1) // set to localhost.com || mysubdomain.localhost.com
}
}
// finally set the .localhost.com (for(1-level) || .mysubdomain.localhost.com (for 2-level subdomain allow)
cookie.Domain = "." + requestDomain // . to allow persistence
}
}
cookie.HttpOnly = true
// MaxAge=0 means no 'Max-Age' attribute specified.
// MaxAge<0 means delete cookie now, equivalently 'Max-Age: 0'
// MaxAge>0 means Max-Age attribute present and given in seconds
if s.config.Expires >= 0 {
if s.config.Expires == 0 { // unlimited life
cookie.Expires = CookieExpireUnlimited
} else { // > 0
cookie.Expires = time.Now().Add(s.config.Expires)
}
cookie.MaxAge = int(cookie.Expires.Sub(time.Now()).Seconds())
}
// set the cookie to secure if this is a tls wrapped request
// and the configuration allows it.
if req.TLS != nil && s.config.CookieSecureTLS {
cookie.Secure = true
}
// encode the session id cookie client value right before send it.
cookie.Value = s.encodeCookieValue(cookie.Value)
AddCookie(cookie, res)
} else {
cookieValue = s.decodeCookieValue(cookieValue)
sess = s.provider.Read(cookieValue, s.config.Expires)
}
return sess
}
// Destroy remove the session data and remove the associated cookie.
func (s *Manager) Destroy(res http.ResponseWriter, req *http.Request) {
cookieValue := GetCookie(s.config.Cookie, req)
// decode the client's cookie value in order to find the server's session id
// to destroy the session data.
cookieValue = s.decodeCookieValue(cookieValue)
if cookieValue == "" { // nothing to destroy
return
}
RemoveCookie(s.config.Cookie, res, req)
s.provider.Destroy(cookieValue)
}
// DestroyByID removes the session entry
// from the server-side memory (and database if registered).
// Client's session cookie will still exist but it will be reseted on the next request.
//
// It's safe to use it even if you are not sure if a session with that id exists.
//
// Note: the sid should be the original one (i.e: fetched by a store )
// it's not decoded.
func (s *Manager) DestroyByID(sid string) {
s.provider.Destroy(sid)
}
// DestroyAll removes all sessions
// from the server-side memory (and database if registered).
// Client's session cookie will still exist but it will be reseted on the next request.
func (s *Manager) DestroyAll() {
s.provider.DestroyAll()
}
// let's keep these funcs simple, we can do it with two lines but we may add more things in the future.
func (s *Manager) decodeCookieValue(cookieValue string) string {
var cookieValueDecoded *string
if decode := s.config.Decode; decode != nil {
err := decode(s.config.Cookie, cookieValue, &cookieValueDecoded)
if err == nil {
cookieValue = *cookieValueDecoded
} else {
cookieValue = ""
}
}
return cookieValue
}
func (s *Manager) encodeCookieValue(cookieValue string) string {
if encode := s.config.Encode; encode != nil {
newVal, err := encode(s.config.Cookie, cookieValue)
if err == nil {
cookieValue = newVal
} else {
cookieValue = ""
}
}
return cookieValue
}