Forms are YAML in forms/, rendered by {{ form }} with a small script and stylesheet the build publishes. cl-cms endpoint serves /_cl/ for any number of sites by host: it checks Origin, a per-IP rate limit, text-only bodies, a trap field, a signed 3 s to 24 h token and optional Turnstile, then the declared fields (types, lengths, options, no line breaks in single-line fields), and delivers by SMTP over TLS, optionally appending to a 0600 JSONL file. It also answers search from a collection's search.json, so a site can search in the browser or from the endpoint. cl-cms serve can pass /_cl/ to an endpoint, and -reload=false leaves out the live-reload script for headless browsers.
50 lines
1.2 KiB
Go
50 lines
1.2 KiB
Go
package endpoint
|
|
|
|
import (
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// limiter counts attempts per key in a sliding window, in memory. Behind a
|
|
// load balancer each endpoint counts on its own, so the effective limit is
|
|
// the configured one times the number of endpoints; for a contact form that
|
|
// is fine. A shared limit would need shared state, which is the first thing
|
|
// here that would ask for a data store.
|
|
type limiter struct {
|
|
mu sync.Mutex
|
|
count int
|
|
window time.Duration
|
|
hits map[string][]time.Time
|
|
calls int
|
|
}
|
|
|
|
func newLimiter(count int, window time.Duration) *limiter {
|
|
return &limiter{count: count, window: window, hits: map[string][]time.Time{}}
|
|
}
|
|
|
|
// allow records an attempt and reports whether it is within the limit.
|
|
func (l *limiter) allow(key string, now time.Time) bool {
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
l.calls++
|
|
if l.calls%1000 == 0 { // now and then, forget addresses that have gone quiet
|
|
for k, ts := range l.hits {
|
|
if len(ts) == 0 || now.Sub(ts[len(ts)-1]) > l.window {
|
|
delete(l.hits, k)
|
|
}
|
|
}
|
|
}
|
|
ts := l.hits[key]
|
|
cut := 0
|
|
for cut < len(ts) && now.Sub(ts[cut]) > l.window {
|
|
cut++
|
|
}
|
|
ts = ts[cut:]
|
|
if len(ts) >= l.count {
|
|
l.hits[key] = ts
|
|
return false
|
|
}
|
|
l.hits[key] = append(ts, now)
|
|
return true
|
|
}
|