Files
scriptos fd13e67aef
Release Docker Image / Build & Push Docker Image (release) Failing after 1m30s
Release: v0.1.0-alpha
2026-04-05 16:56:16 +02:00

97 lines
2.3 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Keywarden - Centralized SSH Key Management and Deployment
// Copyright (C) 2026 Patrick Asmus (scriptos)
// SPDX-License-Identifier: AGPL-3.0-or-later
package security
import (
"net/http"
"strings"
"sync"
"time"
)
// rateLimiter implements a fixed-window IP-based rate limiter.
type rateLimiter struct {
mu sync.Mutex
clients map[string]*window
limit int
period time.Duration
}
type window struct {
count int
resetAt time.Time
}
func newRateLimiter(limit int, period time.Duration) *rateLimiter {
rl := &rateLimiter{
clients: make(map[string]*window),
limit: limit,
period: period,
}
// Background goroutine to evict expired entries (prevent memory leak)
go func() {
ticker := time.NewTicker(5 * time.Minute)
defer ticker.Stop()
for range ticker.C {
now := time.Now()
rl.mu.Lock()
for ip, w := range rl.clients {
if now.After(w.resetAt) {
delete(rl.clients, ip)
}
}
rl.mu.Unlock()
}
}()
return rl
}
func (rl *rateLimiter) allow(ip string) bool {
rl.mu.Lock()
defer rl.mu.Unlock()
now := time.Now()
w, ok := rl.clients[ip]
if !ok || now.After(w.resetAt) {
rl.clients[ip] = &window{count: 1, resetAt: now.Add(rl.period)}
return true
}
w.count++
return w.count <= rl.limit
}
// RateLimitMiddleware returns middleware that rate-limits requests to the
// login endpoint by client IP address.
//
// loginLimit is the maximum number of login attempts per IP per minute.
// A value of 0 disables rate limiting entirely.
func RateLimitMiddleware(loginLimit int) func(http.Handler) http.Handler {
if loginLimit <= 0 {
// Disabled pass through
return func(next http.Handler) http.Handler { return next }
}
loginRL := newRateLimiter(loginLimit, 1*time.Minute)
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Only rate-limit POST to login endpoints
if r.Method == http.MethodPost && isLoginPath(r.URL.Path) {
ip := ClientIP(r)
if !loginRL.allow(ip) {
http.Error(w, "Too Many Requests please try again later", http.StatusTooManyRequests)
return
}
}
next.ServeHTTP(w, r)
})
}
}
// isLoginPath returns true for login-related POST paths.
func isLoginPath(path string) bool {
return path == "/login" || strings.HasPrefix(path, "/login/")
}