tokenmanager.go 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  1. // Copyright (C) 2024 The Syncthing Authors.
  2. //
  3. // This Source Code Form is subject to the terms of the Mozilla Public
  4. // License, v. 2.0. If a copy of the MPL was not distributed with this file,
  5. // You can obtain one at https://mozilla.org/MPL/2.0/.
  6. package api
  7. import (
  8. "net/http"
  9. "slices"
  10. "strings"
  11. "time"
  12. "github.com/syncthing/syncthing/lib/config"
  13. "github.com/syncthing/syncthing/lib/db"
  14. "github.com/syncthing/syncthing/lib/events"
  15. "github.com/syncthing/syncthing/lib/rand"
  16. "github.com/syncthing/syncthing/lib/sync"
  17. )
  18. type tokenManager struct {
  19. key string
  20. miscDB *db.NamespacedKV
  21. lifetime time.Duration
  22. maxItems int
  23. timeNow func() time.Time // can be overridden for testing
  24. mut sync.Mutex
  25. tokens *TokenSet
  26. saveTimer *time.Timer
  27. }
  28. func newTokenManager(key string, miscDB *db.NamespacedKV, lifetime time.Duration, maxItems int) *tokenManager {
  29. tokens := &TokenSet{
  30. Tokens: make(map[string]int64),
  31. }
  32. if bs, ok, _ := miscDB.Bytes(key); ok {
  33. _ = tokens.Unmarshal(bs) // best effort
  34. }
  35. return &tokenManager{
  36. key: key,
  37. miscDB: miscDB,
  38. lifetime: lifetime,
  39. maxItems: maxItems,
  40. timeNow: time.Now,
  41. mut: sync.NewMutex(),
  42. tokens: tokens,
  43. }
  44. }
  45. // Check returns true if the token is valid, and updates the token's expiry
  46. // time. The token is removed if it is expired.
  47. func (m *tokenManager) Check(token string) bool {
  48. m.mut.Lock()
  49. defer m.mut.Unlock()
  50. expires, ok := m.tokens.Tokens[token]
  51. if ok {
  52. if expires < m.timeNow().UnixNano() {
  53. // The token is expired.
  54. m.saveLocked() // removes expired tokens
  55. return false
  56. }
  57. // Give the token further life.
  58. m.tokens.Tokens[token] = m.timeNow().Add(m.lifetime).UnixNano()
  59. m.saveLocked()
  60. }
  61. return ok
  62. }
  63. // New creates a new token and returns it.
  64. func (m *tokenManager) New() string {
  65. token := rand.String(randomTokenLength)
  66. m.mut.Lock()
  67. defer m.mut.Unlock()
  68. m.tokens.Tokens[token] = m.timeNow().Add(m.lifetime).UnixNano()
  69. m.saveLocked()
  70. return token
  71. }
  72. // Delete removes a token.
  73. func (m *tokenManager) Delete(token string) {
  74. m.mut.Lock()
  75. defer m.mut.Unlock()
  76. delete(m.tokens.Tokens, token)
  77. m.saveLocked()
  78. }
  79. func (m *tokenManager) saveLocked() {
  80. // Remove expired tokens.
  81. now := m.timeNow().UnixNano()
  82. for token, expiry := range m.tokens.Tokens {
  83. if expiry < now {
  84. delete(m.tokens.Tokens, token)
  85. }
  86. }
  87. // If we have a limit on the number of tokens, remove the oldest ones.
  88. if m.maxItems > 0 && len(m.tokens.Tokens) > m.maxItems {
  89. // Sort the tokens by expiry time, oldest first.
  90. type tokenExpiry struct {
  91. token string
  92. expiry int64
  93. }
  94. var tokens []tokenExpiry
  95. for token, expiry := range m.tokens.Tokens {
  96. tokens = append(tokens, tokenExpiry{token, expiry})
  97. }
  98. slices.SortFunc(tokens, func(i, j tokenExpiry) int {
  99. return int(i.expiry - j.expiry)
  100. })
  101. // Remove the oldest tokens.
  102. for _, token := range tokens[:len(tokens)-m.maxItems] {
  103. delete(m.tokens.Tokens, token.token)
  104. }
  105. }
  106. // Postpone saving until one second of inactivity.
  107. if m.saveTimer == nil {
  108. m.saveTimer = time.AfterFunc(time.Second, m.scheduledSave)
  109. } else {
  110. m.saveTimer.Reset(time.Second)
  111. }
  112. }
  113. func (m *tokenManager) scheduledSave() {
  114. m.mut.Lock()
  115. defer m.mut.Unlock()
  116. m.saveTimer = nil
  117. bs, _ := m.tokens.Marshal() // can't fail
  118. _ = m.miscDB.PutBytes(m.key, bs) // can fail, but what are we going to do?
  119. }
  120. type tokenCookieManager struct {
  121. cookieName string
  122. shortID string
  123. guiCfg config.GUIConfiguration
  124. evLogger events.Logger
  125. tokens *tokenManager
  126. }
  127. func newTokenCookieManager(shortID string, guiCfg config.GUIConfiguration, evLogger events.Logger, miscDB *db.NamespacedKV) *tokenCookieManager {
  128. return &tokenCookieManager{
  129. cookieName: "sessionid-" + shortID,
  130. shortID: shortID,
  131. guiCfg: guiCfg,
  132. evLogger: evLogger,
  133. tokens: newTokenManager("sessions", miscDB, maxSessionLifetime, maxActiveSessions),
  134. }
  135. }
  136. func (m *tokenCookieManager) createSession(username string, persistent bool, w http.ResponseWriter, r *http.Request) {
  137. sessionid := m.tokens.New()
  138. // Best effort detection of whether the connection is HTTPS --
  139. // either directly to us, or as used by the client towards a reverse
  140. // proxy who sends us headers.
  141. connectionIsHTTPS := r.TLS != nil ||
  142. strings.ToLower(r.Header.Get("x-forwarded-proto")) == "https" ||
  143. strings.Contains(strings.ToLower(r.Header.Get("forwarded")), "proto=https")
  144. // If the connection is HTTPS, or *should* be HTTPS, set the Secure
  145. // bit in cookies.
  146. useSecureCookie := connectionIsHTTPS || m.guiCfg.UseTLS()
  147. maxAge := 0
  148. if persistent {
  149. maxAge = int(maxSessionLifetime.Seconds())
  150. }
  151. http.SetCookie(w, &http.Cookie{
  152. Name: m.cookieName,
  153. Value: sessionid,
  154. // In HTTP spec Max-Age <= 0 means delete immediately,
  155. // but in http.Cookie MaxAge = 0 means unspecified (session) and MaxAge < 0 means delete immediately
  156. MaxAge: maxAge,
  157. Secure: useSecureCookie,
  158. Path: "/",
  159. })
  160. emitLoginAttempt(true, username, r.RemoteAddr, m.evLogger)
  161. }
  162. func (m *tokenCookieManager) hasValidSession(r *http.Request) bool {
  163. for _, cookie := range r.Cookies() {
  164. // We iterate here since there may, historically, be multiple
  165. // cookies with the same name but different path. Any "old" ones
  166. // won't match an existing session and will be ignored, then
  167. // later removed on logout or when timing out.
  168. if cookie.Name == m.cookieName {
  169. if m.tokens.Check(cookie.Value) {
  170. return true
  171. }
  172. }
  173. }
  174. return false
  175. }
  176. func (m *tokenCookieManager) destroySession(w http.ResponseWriter, r *http.Request) {
  177. for _, cookie := range r.Cookies() {
  178. // We iterate here since there may, historically, be multiple
  179. // cookies with the same name but different path. We drop them
  180. // all.
  181. if cookie.Name == m.cookieName {
  182. m.tokens.Delete(cookie.Value)
  183. // Create a cookie deletion command
  184. http.SetCookie(w, &http.Cookie{
  185. Name: m.cookieName,
  186. Value: "",
  187. MaxAge: -1,
  188. Secure: cookie.Secure,
  189. Path: cookie.Path,
  190. })
  191. }
  192. }
  193. }