2 * Copyright (C) 2017-2018 "IoT.bzh"
3 * Author Sebastien Douheret <sebastien@iot.bzh>
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
9 * http://www.apache.org/licenses/LICENSE-2.0
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
25 "github.com/gin-gonic/gin"
26 "github.com/googollee/go-socket.io"
27 uuid "github.com/satori/go.uuid"
28 "github.com/syncthing/syncthing/lib/sync"
31 const sessionCookieName = "xds-agent-sid"
32 const sessionHeaderName = "XDS-AGENT-SID"
34 const sessionMonitorTime = 10 // Time (in seconds) to schedule monitoring session tasks
36 const initSessionMaxAge = 10 // Initial session max age in seconds
37 const maxSessions = 100000 // Maximum number of sessions in sessMap map
39 const secureCookie = false // TODO: see https://github.com/astaxie/beego/blob/master/session/session.go#L218
41 // ClientSession contains the info of a user/client session
42 type ClientSession struct {
44 WSID string // only one WebSocket per client/session
46 IOSocket *socketio.Socket
53 // Sessions holds client sessions
54 type Sessions struct {
57 sessMap map[string]ClientSession
59 stop chan struct{} // signals intentional stop
62 // NewClientSessions .
63 func NewClientSessions(ctx *Context, cookieMaxAge string) *Sessions {
64 ckMaxAge, err := strconv.ParseInt(cookieMaxAge, 10, 0)
70 cookieMaxAge: ckMaxAge,
71 sessMap: make(map[string]ClientSession),
72 mutex: sync.NewMutex(),
73 stop: make(chan struct{}),
75 s.webServer.router.Use(s.Middleware())
77 // Start monitoring of sessions Map (use to manage expiration and cleanup)
83 // Stop sessions management
84 func (s *Sessions) Stop() {
88 // Middleware is used to managed session
89 func (s *Sessions) Middleware() gin.HandlerFunc {
90 return func(c *gin.Context) {
91 // FIXME Add CSRF management
96 // Allocate a new session key and put in cookie
97 sess = s.newSession("")
102 // Set session in cookie and in header
103 // Do not set Domain to localhost (http://stackoverflow.com/questions/1134290/cookies-on-localhost-with-explicit-domain)
104 c.SetCookie(sessionCookieName, sess.ID, int(sess.MaxAge), "/", "",
106 c.Header(sessionHeaderName, sess.ID)
108 // Save session id in gin metadata
109 c.Set(sessionCookieName, sess.ID)
115 // Get returns the client session for a specific ID
116 func (s *Sessions) Get(c *gin.Context) *ClientSession {
119 // First get from gin metadata
120 v, exist := c.Get(sessionCookieName)
125 // Then look in cookie
126 if !exist || sid == "" {
127 sid, _ = c.Cookie(sessionCookieName)
130 // Then look in Header
132 sid = c.Request.Header.Get(sessionCookieName)
136 defer s.mutex.Unlock()
137 if key, ok := s.sessMap[sid]; ok {
138 // TODO: return a copy ???
145 // GetID returns the session or an empty string
146 func (s *Sessions) GetID(c *gin.Context) string {
147 if sess := s.Get(c); sess != nil {
153 // IOSocketGet Get socketio definition from sid
154 func (s *Sessions) IOSocketGet(sid string) *socketio.Socket {
156 defer s.mutex.Unlock()
157 sess, ok := s.sessMap[sid]
164 // UpdateIOSocket updates the IO Socket definition for of a session
165 func (s *Sessions) UpdateIOSocket(sid string, so *socketio.Socket) error {
167 defer s.mutex.Unlock()
168 if _, ok := s.sessMap[sid]; ok {
169 sess := s.sessMap[sid]
171 // Could be the case when socketio is closed/disconnected
174 sess.WSID = (*so).Id()
177 s.sessMap[sid] = sess
182 // newSession Allocate a new client session
183 func (s *Sessions) newSession(prefix string) *ClientSession {
184 uuid := prefix + uuid.NewV4().String()
185 id := base64.URLEncoding.EncodeToString([]byte(uuid))
189 MaxAge: initSessionMaxAge,
191 expireAt: time.Now().Add(time.Duration(initSessionMaxAge) * time.Second),
195 defer s.mutex.Unlock()
197 s.sessMap[se.ID] = se
199 s.Log.Debugf("NEW session (%d): %s", len(s.sessMap), id)
203 // refresh Move this session ID to the head of the list
204 func (s *Sessions) refresh(sid string) {
206 defer s.mutex.Unlock()
208 sess := s.sessMap[sid]
210 if sess.MaxAge < s.cookieMaxAge && sess.useCount > 1 {
211 sess.MaxAge = s.cookieMaxAge
212 sess.expireAt = time.Now().Add(time.Duration(sess.MaxAge) * time.Second)
215 // TODO - Add flood detection (like limit_req of nginx)
216 // (delayed request when to much requests in a short period of time)
218 s.sessMap[sid] = sess
221 func (s *Sessions) monitorSessMap() {
225 s.Log.Debugln("Stop monitorSessMap")
227 case <-time.After(sessionMonitorTime * time.Second):
228 s.LogSillyf("Sessions Map size: %d", len(s.sessMap))
229 s.LogSillyf("Sessions Map : %v", s.sessMap)
231 if len(s.sessMap) > maxSessions {
232 s.Log.Errorln("TOO MUCH sessions, cleanup old ones !")
236 for _, ss := range s.sessMap {
237 if ss.expireAt.Sub(time.Now()) < 0 {
238 s.LogSillyf("Delete expired session id: %s", ss.ID)
239 delete(s.sessMap, ss.ID)