1 parent
cc7466823b
commit
e8c5b414d9
44 files changed
+7649
-344
No files matched your search
+28
-8
@@ -9,7 +9,6 @@ package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/subtle"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -241,7 +240,7 @@ func (s *Server) routes(cfg *config.Config) *http.ServeMux {
|
||||
http.Error(w, "metrics not available", http.StatusNotImplemented)
|
||||
})))
|
||||
}
|
||||
mux.Handle("POST "+base+"auth/login", s.public(s.handleLogin))
|
||||
mux.Handle("POST "+base+"auth/login", s.limited(s.handleLogin))
|
||||
mux.Handle("POST "+base+"auth/logout", s.authed(s.handleLogout, false))
|
||||
|
||||
// Everything below requires a principal.
|
||||
@@ -320,6 +319,22 @@ func (s *Server) cors(next http.Handler) http.Handler {
|
||||
// public marks a handler as reachable without a principal.
|
||||
func (s *Server) public(h http.HandlerFunc) http.Handler { return h }
|
||||
|
||||
// limited applies the per-IP rate limit to a handler that is reachable without
|
||||
// a principal. The login endpoint is the reason the limit exists: guessing
|
||||
// API keys is bounded by nothing but the attacker's patience, so the lockout
|
||||
// the login handler records has to be able to stop the *next* attempt. It did
|
||||
// not, because the limiter ran behind authed() and login is public, so N bad
|
||||
// keys gave N 401s and the lockout only locked the caller out of the rest of
|
||||
// the API.
|
||||
func (s *Server) limited(h http.HandlerFunc) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if !s.allowRequest(w, r) {
|
||||
return
|
||||
}
|
||||
h(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// cfg is a small accessor so handlers read the current config once.
|
||||
|
||||
// authed wraps a handler with authentication and, for mutations, a write
|
||||
@@ -503,8 +518,17 @@ func (s *Server) recordLoginFailure(ip string) {
|
||||
|
||||
func (s *Server) clearLoginFailures(ip string) {
|
||||
s.limMu.Lock()
|
||||
delete(s.limits, ip)
|
||||
s.limMu.Unlock()
|
||||
defer s.limMu.Unlock()
|
||||
st, ok := s.limits[ip]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// Only the auth-failure state is reset. Deleting the whole entry would
|
||||
// also reset the per-minute request window, so a single successful login
|
||||
// in the middle of a burst would hand the client a fresh budget — which
|
||||
// is exactly what an attacker holding one valid key wants.
|
||||
st.failures = 0
|
||||
st.lockedTill = time.Time{}
|
||||
}
|
||||
|
||||
// protectMetrics gates /metrics behind an API key when configured.
|
||||
@@ -597,10 +621,6 @@ func queryTime(r *http.Request, key string) time.Time {
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
func constantTimeEqual(a, b string) bool {
|
||||
return subtle.ConstantTimeCompare([]byte(a), []byte(b)) == 1
|
||||
}
|
||||
|
||||
func isFileWritable(path string) bool {
|
||||
if path == "" {
|
||||
return false
|
||||
|
||||
Reference in new issue
Block a user