web_session_middleware.go 2.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108
  1. // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
  2. // SPDX-License-Identifier: Apache-2.0
  3. package ui // import "miniflux.app/v2/internal/ui"
  4. import (
  5. "context"
  6. "log/slog"
  7. "net/http"
  8. "strings"
  9. "miniflux.app/v2/internal/http/request"
  10. "miniflux.app/v2/internal/http/response"
  11. "miniflux.app/v2/internal/model"
  12. "miniflux.app/v2/internal/storage"
  13. )
  14. type webSessionMiddleware struct {
  15. basePath string
  16. store *storage.Storage
  17. }
  18. func newWebSessionMiddleware(basePath string, store *storage.Storage) *webSessionMiddleware {
  19. return &webSessionMiddleware{basePath: basePath, store: store}
  20. }
  21. func (m *webSessionMiddleware) handle(next http.Handler) http.Handler {
  22. return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  23. if isStaticAssetRoute(r) {
  24. next.ServeHTTP(w, r)
  25. return
  26. }
  27. session, err := m.loadWebSessionFromCookie(r)
  28. if err != nil {
  29. response.HTMLServerError(w, r, err)
  30. return
  31. }
  32. persisted := session != nil
  33. if session == nil {
  34. // Visitors without a session are sent to the login page and have
  35. // nothing to remember yet, so don't persist a session for them.
  36. if !isPublicRoute(r) {
  37. response.HTMLRedirect(w, r, loginRedirectURL(m.basePath, r.RequestURI))
  38. return
  39. }
  40. var secret string
  41. session, secret = model.NewWebSession(r.UserAgent(), request.ClientIP(r))
  42. if language, theme, ok := preferencesFromCookie(r); ok {
  43. session.SeedPreferences(language, theme)
  44. }
  45. // Other public routes either only render pages or expect a session
  46. // started earlier, so an in-memory session is enough for them.
  47. if isSessionStartRoute(r) {
  48. if err := m.store.CreateWebSession(session); err != nil {
  49. response.HTMLServerError(w, r, err)
  50. return
  51. }
  52. setSessionCookie(w, session, secret)
  53. persisted = true
  54. }
  55. }
  56. ctx := context.WithValue(r.Context(), request.WebSessionContextKey, session)
  57. r = r.WithContext(ctx)
  58. if !request.IsAuthenticated(r) && !isPublicRoute(r) {
  59. response.HTMLRedirect(w, r, loginRedirectURL(m.basePath, r.RequestURI))
  60. return
  61. }
  62. next.ServeHTTP(w, r)
  63. if persisted && session.IsDirty() {
  64. if err := m.store.UpdateWebSession(session); err != nil {
  65. slog.Error("Unable to persist web session changes",
  66. slog.String("session_id", session.ID),
  67. slog.Any("error", err),
  68. )
  69. }
  70. }
  71. })
  72. }
  73. func (m *webSessionMiddleware) loadWebSessionFromCookie(r *http.Request) (*model.WebSession, error) {
  74. cookieValue := request.CookieValue(r, sessionCookieName)
  75. if cookieValue == "" {
  76. return nil, nil
  77. }
  78. sessionID, secret, ok := strings.Cut(cookieValue, ".")
  79. if !ok || sessionID == "" || secret == "" {
  80. return nil, nil
  81. }
  82. session, err := m.store.WebSessionByID(sessionID)
  83. if err != nil {
  84. return nil, err
  85. }
  86. if session == nil || !session.VerifySecret(secret) {
  87. return nil, nil
  88. }
  89. return session, nil
  90. }