added media fixes, search, push updates, pwa, metrics. preparing release 1.0
CI / Go (build + vet + test) (push) Canceled after 0s
CI / Web (build + lint) (push) Canceled after 0s

This commit is contained in:
vantablackTea
2026-09-27 23:26:15 +03:00
parent 855a62755c
commit 6fc6d977bf
59 changed files with 6823 additions and 113 deletions
@@ -0,0 +1,61 @@
// Package ratelimit provides a simple in-memory fixed-window limiter
// keyed by client IP, used to protect authentication endpoints.
package ratelimit
import (
"net"
"net/http"
"strings"
"sync"
"time"
)
// Limiter is a fixed-window, per-IP request limiter.
type Limiter struct {
mu sync.Mutex
max int
window time.Duration
hits map[string]*hits
}
type hits struct {
windowStart time.Time
count int
}
// New returns a limiter allowing max requests per window per IP.
func New(max int, window time.Duration) *Limiter {
return &Limiter{max: max, window: window, hits: make(map[string]*hits)}
}
// Allow reports whether the request passes the limit, consuming one slot.
func (l *Limiter) Allow(r *http.Request) bool {
ip := ClientIP(r)
now := time.Now()
l.mu.Lock()
defer l.mu.Unlock()
h := l.hits[ip]
if h == nil || now.Sub(h.windowStart) >= l.window {
h = &hits{windowStart: now}
l.hits[ip] = h
}
h.count++
return h.count <= l.max
}
// ClientIP extracts the remote address, preferring X-Forwarded-For when
// running behind a reverse proxy.
func ClientIP(r *http.Request) string {
if fwd := r.Header.Get("X-Forwarded-For"); fwd != "" {
if i := strings.IndexByte(fwd, ','); i > 0 {
return strings.TrimSpace(fwd[:i])
}
return strings.TrimSpace(fwd)
}
host, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
return r.RemoteAddr
}
return host
}
@@ -0,0 +1,66 @@
package ratelimit
import (
"net/http"
"net/http/httptest"
"testing"
"time"
)
func TestLimiterAllowsUpToMax(t *testing.T) {
l := New(3, time.Minute)
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.RemoteAddr = "1.2.3.4:1234"
for i := 0; i < 3; i++ {
if !l.Allow(req) {
t.Fatalf("request %d rejected, want allowed", i+1)
}
}
if l.Allow(req) {
t.Fatal("4th request allowed, want rejected")
}
}
func TestLimiterSeparatesIPs(t *testing.T) {
l := New(2, time.Minute)
reqA := httptest.NewRequest(http.MethodGet, "/", nil)
reqA.RemoteAddr = "1.1.1.1:1"
reqB := httptest.NewRequest(http.MethodGet, "/", nil)
reqB.RemoteAddr = "2.2.2.2:2"
l.Allow(reqA)
l.Allow(reqA)
if l.Allow(reqA) {
t.Fatal("IP A over limit, want rejected")
}
if !l.Allow(reqB) {
t.Fatal("IP B should have its own budget")
}
}
func TestLimiterWindowResets(t *testing.T) {
l := New(1, 50*time.Millisecond)
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.RemoteAddr = "9.9.9.9:9"
if !l.Allow(req) {
t.Fatal("first request rejected")
}
if l.Allow(req) {
t.Fatal("second request within window allowed, want rejected")
}
time.Sleep(60 * time.Millisecond)
if !l.Allow(req) {
t.Fatal("request after window reset rejected")
}
}
func TestClientIPPrefersForwarded(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.RemoteAddr = "10.0.0.5:1234"
req.Header.Set("X-Forwarded-For", "203.0.113.9, 10.0.0.1")
if got := ClientIP(req); got != "203.0.113.9" {
t.Fatalf("ClientIP = %q, want 203.0.113.9", got)
}
}