/*
* Copyright (C) 2017-2018 "IoT.bzh"
* Author Sebastien Douheret <sebastien@iot.bzh>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xdsserver
import (
"encoding/base64"
"strconv"
"time"
"github.com/gin-gonic/gin"
socketio "github.com/googollee/go-socket.io"
uuid "github.com/satori/go.uuid"
"github.com/syncthing/syncthing/lib/sync"
)
const sessionCookieName = "xds-sid"
const sessionHeaderName = "XDS-SID"
const sessionMonitorTime = 10 // Time (in seconds) to schedule monitoring session tasks
const initSessionMaxAge = 10 // Initial session max age in seconds
const maxSessions = 100000 // Maximum number of sessions in sessMap map
const secureCookie = false // TODO: see https://github.com/astaxie/beego/blob/master/session/session.go#L218
// ClientSession contains the info of a user/client session
type ClientSession struct {
ID string
WSID string // only one WebSocket per client/session
MaxAge int64
IOSocket *socketio.Socket
// private
expireAt time.Time
useCount int64
}
// Sessions holds client sessions
type Sessions struct {
*Context
cookieMaxAge int64
sessMap map[string]ClientSession
mutex sync.Mutex
stop chan struct{} // signals intentional stop
}
// ClientSessionsConstructor .
func ClientSessionsConstructor(ctx *Context, cookieMaxAge string) *Sessions {
ckMaxAge, err := strconv.ParseInt(cookieMaxAge, 10, 0)
if err != nil {
ckMaxAge = 0
}
s := Sessions{
Context: ctx,
cookieMaxAge: ckMaxAge,
sessMap: make(map[string]ClientSession),
mutex: sync.NewMutex(),
stop: make(chan struct{}),
}
s.WWWServer.router.Use(s.Middleware())
// Start monitoring of sessions Map (use to manage expiration and cleanup)
go s.monitorSessMap()
return &s
}
// Stop sessions management
func (s *Sessions) Stop() {
close(s.stop)
}
// Middleware is used to managed session
func (s *Sessions) Middleware() gin.HandlerFunc {
return func(c *gin.Context) {
// FIXME Add CSRF management
// Get session
sess := s.Get(c)
if sess == nil {
// Allocate a new session key and put in cookie
sess = s.newSession("")
} else {
s.refresh(sess.ID)
}
// Set session in cookie and in header
// Do not set Domain to localhost (http://stackoverflow.com/questions/1134290/cookies-on-localhost-with-explicit-domain)
c.SetCookie(sessionCookieName, sess.ID, int(sess.MaxAge), "/", "",
secureCookie, false)
c.Header(sessionHeaderName, sess.ID)
// Save session id in gin metadata
c.Set(sessionCookieName, sess.ID)
c.Next()
}
}
// Get returns the client session for a specific ID
func (s *Sessions) Get(c *gin.Context) *ClientSession {
var sid string
// First get from gin metadata
v, exist := c.Get(sessionCookieName)
if v != nil {
sid = v.(string)
}
// Then look in cookie
if !exist || sid == "" {
sid, _ = c.Cookie(sessionCookieName)
}
// Then look in Header
if sid == "" {
sid = c.Request.Header.Get(sessionCookieName)
}
if sid != "" {
s.mutex.Lock()
defer s.mutex.Unlock()
if key, ok := s.sessMap[sid]; ok {
// TODO: return a copy ???
return &key
}
}
return nil
}
// IOSocketGet Get socketio definition from sid
func (s *Sessions) IOSocketGet(sid string) *socketio.Socket {
s.mutex.Lock()
defer s.mutex.Unlock()
sess, ok := s.sessMap[sid]
if ok {
return sess.IOSocket
}
return nil
}
// UpdateIOSocket updates the IO Socket definition for of a session
func (s *Sessions) UpdateIOSocket(sid string, so *socketio.Socket) error {
s.mutex.Lock()
defer s.mutex.Unlock()
if _, ok := s.sessMap[sid]; ok {
sess := s.sessMap[sid]
if so == nil {
// Could be the case when socketio is closed/disconnected
sess.WSID = ""
} else {
sess.WSID = (*so).Id()
}
sess.IOSocket = so
s.sessMap[sid] = sess
}
return nil
}
// nesSession Allocate a new client session
func (s *Sessions) newSession(prefix string) *ClientSession {
uuid := prefix + uuid.NewV4().String()
id := base64.URLEncoding.EncodeToString([]byte(uuid))
se := ClientSession{
ID: id,
WSID: "",
MaxAge: initSessionMaxAge,
IOSocket: nil,
expireAt: time.Now().Add(time.Duration(initSessionMaxAge) * time.Second),
useCount: 0,
}
s.mutex.Lock()
defer s.mutex.Unlock()
s.sessMap[se.ID] = se
s.Log.Debugf("NEW session (%d): %s", len(s.sessMap), id)
return &se
}
// refresh Move this session ID to the head of the list
func (s *Sessions) refresh(sid string) {
s.mutex.Lock()
defer s.mutex.Unlock()
sess := s.sessMap[sid]
sess.useCount++
if sess.MaxAge < s.cookieMaxAge && sess.useCount > 1 {
sess.MaxAge = s.cookieMaxAge
sess.expireAt = time.Now().Add(time.Duration(sess.MaxAge) * time.Second)
}
// TODO - Add flood detection (like limit_req of nginx)
// (delayed request when to much requests in a short period of time)
s.sessMap[sid] = sess
}
func (s *Sessions) monitorSessMap() {
for {
select {
case <-s.stop:
s.Log.Debugln("Stop monitorSessMap")
return
case <-time.After(sessionMonitorTime * time.Second):
s.LogSillyf("Sessions Map size: %d", len(s.sessMap))
s.LogSillyf("Sessions Map : %v", s.sessMap)
if len(s.sessMap) > maxSessions {
s.Log.Errorln("TOO MUCH sessions, cleanup old ones !")
}
s.mutex.Lock()
for _, ss := range s.sessMap {
if ss.expireAt.Sub(time.Now()) <= 0 {
s.Log.Debugf("Delete expired session id: %s", ss.ID)
delete(s.sessMap, ss.ID)
}
}
s.mutex.Unlock()
}
}
}