tokenmanager.go 6.0 KB

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