Files
2026-09-16 06:26:16 +02:00

86 lines
1.8 KiB
Go

package httpserver
import (
"bytes"
"io"
"net"
"net/http"
"net/url"
"strings"
"time"
)
type loginBucket struct {
Start time.Time
Count int
}
func (s *Server) allowLogin(address string) bool {
ip, _, err := net.SplitHostPort(address)
if err != nil {
ip = address
}
s.loginMu.Lock()
defer s.loginMu.Unlock()
if s.logins == nil {
s.logins = map[string]loginBucket{}
}
now := time.Now()
for k, v := range s.logins {
if now.Sub(v.Start) > time.Minute {
delete(s.logins, k)
}
}
b := s.logins[ip]
if b.Start.IsZero() {
if len(s.logins) >= 1024 {
return false
}
b.Start = now
}
b.Count++
s.logins[ip] = b
return b.Count <= 10
}
func requestProtection(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// No proxy headers are trusted. Configure the proxy to preserve the Host.
admin := strings.HasPrefix(r.URL.Path, "/ui/") || r.URL.Path == "/login" || r.URL.Path == "/logout"
if admin {
w.Header().Set("Cache-Control", "no-store")
}
if admin && r.Method != "GET" && r.Method != "HEAD" {
if origin := r.Header.Get("Origin"); origin != "" {
u, err := url.Parse(origin)
if err != nil || u.Host != r.Host || (u.Scheme != "http" && u.Scheme != "https") {
http.Error(w, "cross-origin request rejected", 403)
return
}
}
if r.Header.Get("Sec-Fetch-Site") == "cross-site" {
http.Error(w, "cross-site request rejected", 403)
return
}
}
if r.Body != nil {
limit := int64(1 << 20)
if admin {
limit = 2 << 20
}
b, err := io.ReadAll(io.LimitReader(r.Body, limit+1))
r.Body.Close()
if err != nil {
http.Error(w, "invalid body", 400)
return
}
if int64(len(b)) > limit {
http.Error(w, "request too large", 413)
return
}
r.Body = io.NopCloser(bytes.NewReader(b))
}
next.ServeHTTP(w, r)
})
}