middleware.go 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
  1. package controller
  2. import (
  3. "net"
  4. "net/http"
  5. "net/url"
  6. "os"
  7. "strings"
  8. "sync"
  9. "time"
  10. "github.com/gin-gonic/gin"
  11. )
  12. func (c *Controller) IsAuthenticated(ctx *gin.Context) {
  13. cookie, err := ctx.Cookie(AUTH_COOKIE_NAME)
  14. if err != nil || !c.Cache.Read(cookie) {
  15. ctx.Redirect(http.StatusFound, "/login")
  16. ctx.Abort()
  17. return
  18. }
  19. ctx.Next()
  20. }
  21. // insecureCookies is solely for local HTTP development. Production must use HTTPS.
  22. func insecureCookies() bool { return os.Getenv("KEIJI_INSECURE_COOKIE") == "true" }
  23. // SameOrigin protects unsafe browser requests, including ordinary forms and HTMX.
  24. // Non-browser clients must supply an Origin matching the public request URL.
  25. func SameOrigin(ctx *gin.Context) {
  26. switch ctx.Request.Method {
  27. case http.MethodGet, http.MethodHead, http.MethodOptions:
  28. ctx.Next()
  29. return
  30. }
  31. source := ctx.GetHeader("Origin")
  32. origin := source != ""
  33. if !origin {
  34. source = ctx.GetHeader("Referer")
  35. }
  36. u, err := url.Parse(source)
  37. scheme := "https"
  38. if insecureCookies() {
  39. scheme = "http"
  40. }
  41. if err != nil || u == nil || u.User != nil || u.Opaque != "" ||
  42. !strings.EqualFold(u.Scheme, scheme) || !strings.EqualFold(u.Host, ctx.Request.Host) ||
  43. u.Host == "" || (origin && (u.Path != "" || u.RawQuery != "" || u.Fragment != "")) {
  44. ctx.AbortWithStatus(http.StatusForbidden)
  45. return
  46. }
  47. ctx.Next()
  48. }
  49. type loginWindow struct {
  50. count int
  51. until time.Time
  52. }
  53. // LoginRateLimit limits all attempts, not just failures, so concurrent attempts
  54. // cannot race a post-authentication counter. State is local to this process.
  55. func LoginRateLimit() gin.HandlerFunc {
  56. var mu sync.Mutex
  57. windows := make(map[string]loginWindow)
  58. var proxies []*net.IPNet
  59. for _, value := range strings.Split(os.Getenv("KEIJI_TRUSTED_PROXIES"), ",") {
  60. value = strings.TrimSpace(value)
  61. if ip := net.ParseIP(value); ip != nil {
  62. bits := 128
  63. if ip.To4() != nil {
  64. ip = ip.To4()
  65. bits = 32
  66. }
  67. proxies = append(proxies, &net.IPNet{IP: ip, Mask: net.CIDRMask(bits, bits)})
  68. } else if _, network, err := net.ParseCIDR(value); err == nil {
  69. proxies = append(proxies, network)
  70. }
  71. }
  72. trusted := func(ip net.IP) bool {
  73. for _, network := range proxies {
  74. if network.Contains(ip) {
  75. return true
  76. }
  77. }
  78. return false
  79. }
  80. return func(ctx *gin.Context) {
  81. peer, _, err := net.SplitHostPort(ctx.Request.RemoteAddr)
  82. if err != nil {
  83. peer = ctx.Request.RemoteAddr
  84. }
  85. ip := net.ParseIP(peer)
  86. if ip != nil && trusted(ip) {
  87. // Walk from the trusted peer toward the client, stopping at the first
  88. // untrusted hop. Never accept forwarding headers from an untrusted peer.
  89. parts := strings.Split(ctx.GetHeader("X-Forwarded-For"), ",")
  90. for i := len(parts) - 1; i >= 0 && trusted(ip); i-- {
  91. next := net.ParseIP(strings.TrimSpace(parts[i]))
  92. if next == nil {
  93. break
  94. }
  95. ip = next
  96. }
  97. }
  98. key := "unknown"
  99. if ip != nil {
  100. key = ip.String()
  101. }
  102. now := time.Now()
  103. mu.Lock()
  104. w := windows[key]
  105. if !now.Before(w.until) {
  106. w = loginWindow{until: now.Add(10 * time.Minute)}
  107. }
  108. if w.count >= 10 {
  109. mu.Unlock()
  110. ctx.Header("Retry-After", "600")
  111. ctx.AbortWithStatus(http.StatusTooManyRequests)
  112. return
  113. }
  114. if _, exists := windows[key]; !exists && len(windows) >= 10000 {
  115. for k, old := range windows {
  116. if !now.Before(old.until) {
  117. delete(windows, k)
  118. }
  119. }
  120. if len(windows) >= 10000 {
  121. mu.Unlock()
  122. ctx.Header("Retry-After", "600")
  123. ctx.AbortWithStatus(http.StatusTooManyRequests)
  124. return
  125. }
  126. }
  127. w.count++
  128. windows[key] = w
  129. mu.Unlock()
  130. ctx.Next()
  131. }
  132. }