aboutsummaryrefslogtreecommitdiffstats
path: root/lib/xdsserver/sessions.go
blob: 0c16b995ec10fa5b12efc48c79e6fd4d5b2963b1 (plain)
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
.highlight .hll { background-color: #ffffcc }
.highlight .c { color: #888888 } /* Comment */
.highlight .err { color: #a61717; background-color: #e3d2d2 } /* Error */
.highlight .k { color: #008800; font-weight: bold } /* Keyword */
.highlight .ch { color: #888888 } /* Comment.Hashbang */
.highlight .cm { color: #888888 } /* Comment.Multiline */
.highlight .cp { color: #cc0000; font-weight: bold } /* Comment.Preproc */
.highlight .cpf { color: #888888 } /* Comment.PreprocFile */
.highlight .c1 { color: #888888 } /* Comment.Single */
.highlight .cs { color: #cc0000; font-weight: bold; background-color: #fff0f0 } /* Comment.Special */
.highlight .gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */
.highlight .ge { font-style: italic } /* Generic.Emph */
.highlight .gr { color: #aa0000 } /* Generic.Error */
.highlight .gh { color: #333333 } /* Generic.Heading */
.highlight .gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */
.highlight .go { color: #888888 } /* Generic.Output */
.highlight .gp { color: #555555 } /* Generic.Prompt */
.highlight .gs { font-weight: bold } /* Generic.Strong */
.highlight .gu { color: #666666 } /* Generic.Subheading */
.highlight .gt { color: #aa0000 } /* Generic.Traceback */
.highlight .kc { color: #008800; font-weight: bold } /* Keyword.Constant */
.highlight .kd { color: #008800; font-weight: bold } /* Keyword.Declaration */
.highlight .kn { color: #008800; font-weight: bold } /* Keyword.Namespace */
.highlight .kp { color: #008800 } /* Keyword.Pseudo */
.highlight .kr { color: #008800; font-weight: bold } /* Keyword.Reserved */
.highlight .kt { color: #888888; font-weight: bold } /* Keyword.Type */
.highlight .m { color: #0000DD; font-weight: bold } /* Literal.Number */
.highlight .s { color: #dd2200; background-color: #fff0f0 } /* Literal.String */
.highlight .na { color: #336699 } /* Name.Attribute */
.highlight .nb { color: #003388 } /* Name.Builtin */
.highlight .nc { color: #bb0066; font-weight: bold } /* Name.Class */
.highlight .no { color: #003366; font-weight: bold } /* Name.Constant */
.highlight .nd { color: #555555 } /* Name.Decorator */
.highlight .ne { color: #bb0066; font-weight: bold } /* Name.Exception */
.highlight .nf { color: #0066bb; font-weight: bold } /* Name.Function */
.highlight .nl { color: #336699; font-style: italic } /* Name.Label */
.highlight .nn { color: #bb0066; font-weight: bold } /* Name.Namespace */
.highlight .py { color: #336699; font-weight: bold } /* Name.Property */
.highlight .nt { color: #bb0066; font-weight: bold } /* Name.Tag */
.highlight .nv { color: #336699 } /* Name.Variable */
.highlight .ow { color: #008800 } /* Operator.Word */
.highlight .w { color: #bbbbbb } /* Text.Whitespace */
.highlight .mb { color: #0000DD; font-weight: bold } /* Literal.Number.Bin */
.highlight .mf { color: #0000DD; font-weight: bold } /* Literal.Number.Float */
.highlight .mh { color: #0000DD; font-weight: bold } /* Literal.Number.Hex */
.highlight .mi { color: #0000DD; font-weight: bold } /* Literal.Number.Integer */
.highlight .mo { color: #0000DD; font-weight: bold } /* Literal.Number.Oct */
.highlight .sa { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Affix */
.highlight .sb { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Backtick */
.highlight .sc { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Char */
.highlight .dl { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Delimiter */
.highlight .sd { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Doc */
.highlight .s2 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Double */
.highlight .se { color: #0044dd; background-color: #fff0f0 } /* Literal.String.Escape */
.highlight .sh { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Heredoc */
.highlight .s
/*
 * 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()
		}
	}
}