Naming, layout and comments: descriptive full-word names, one item per line in wrapped calls and literals, and doc comments on exported names
Short and single-letter names in application code become full words (tests keep theirs). Wrapped calls, signatures and composite literals put one item per line, and one-line function bodies are expanded. Exported names and groups of constants get doc comments, and comments refer to the renamed identifiers.
This commit is contained in:
1 parent
043e307663
commit
465358609a
78 files changed
+2374
-1402
No files matched your search
@@ -24,6 +24,7 @@ type Limiter interface {
|
||||
// LimiterFunc adapts a function to the Limiter interface.
|
||||
type LimiterFunc func(key string) (bool, time.Duration)
|
||||
|
||||
// Allow implements Limiter by calling f.
|
||||
func (f LimiterFunc) Allow(key string) (bool, time.Duration) { return f(key) }
|
||||
|
||||
// KeyFunc derives the rate limit key from a request.
|
||||
@@ -34,22 +35,24 @@ func KeyByIP(r *http.Request) string { return ClientIP(r) }
|
||||
|
||||
// RateLimit turns a Limiter into middleware. Rejected requests get 429 with
|
||||
// a Retry-After header. A nil limiter yields a pass-through middleware.
|
||||
func RateLimit(l Limiter, key KeyFunc) func(http.Handler) http.Handler {
|
||||
if l == nil {
|
||||
return func(next http.Handler) http.Handler { return next }
|
||||
func RateLimit(limiter Limiter, key KeyFunc) func(http.Handler) http.Handler {
|
||||
if limiter == nil {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return next
|
||||
}
|
||||
}
|
||||
if key == nil {
|
||||
key = KeyByIP
|
||||
}
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ok, retry := l.Allow(key(r))
|
||||
ok, retry := limiter.Allow(key(r))
|
||||
if !ok {
|
||||
secs := int(math.Ceil(retry.Seconds()))
|
||||
if secs < 1 {
|
||||
secs = 1
|
||||
seconds := int(math.Ceil(retry.Seconds()))
|
||||
if seconds < 1 {
|
||||
seconds = 1
|
||||
}
|
||||
w.Header().Set("Retry-After", strconv.Itoa(secs))
|
||||
w.Header().Set("Retry-After", strconv.Itoa(seconds))
|
||||
httpx.WriteProblem(w, r, http.StatusTooManyRequests, "rate limit exceeded")
|
||||
return
|
||||
}
|
||||
@@ -74,22 +77,32 @@ type limiterShard struct {
|
||||
}
|
||||
|
||||
type limiterEntry struct {
|
||||
lim *rate.Limiter
|
||||
last time.Time
|
||||
limiter *rate.Limiter
|
||||
last time.Time
|
||||
}
|
||||
|
||||
// NewKeyedLimiter creates a limiter allowing rps sustained requests per
|
||||
// second with the given burst. A janitor goroutine evicts entries idle for
|
||||
// longer than ttl until ctx is cancelled.
|
||||
func NewKeyedLimiter(ctx context.Context, rps float64, burst int, ttl time.Duration) *KeyedLimiter {
|
||||
k := &KeyedLimiter{limit: rate.Limit(rps), burst: burst, ttl: ttl, seed: maphash.MakeSeed()}
|
||||
for i := range k.shards {
|
||||
k.shards[i].entries = make(map[string]*limiterEntry)
|
||||
// NewKeyedLimiter creates a limiter allowing requestsPerSecond sustained
|
||||
// requests per second with the given burst. A janitor goroutine evicts
|
||||
// entries idle for longer than ttl until ctx is cancelled.
|
||||
func NewKeyedLimiter(
|
||||
ctx context.Context,
|
||||
requestsPerSecond float64,
|
||||
burst int,
|
||||
ttl time.Duration,
|
||||
) *KeyedLimiter {
|
||||
limiter := &KeyedLimiter{
|
||||
limit: rate.Limit(requestsPerSecond),
|
||||
burst: burst,
|
||||
ttl: ttl,
|
||||
seed: maphash.MakeSeed(),
|
||||
}
|
||||
for i := range limiter.shards {
|
||||
limiter.shards[i].entries = make(map[string]*limiterEntry)
|
||||
}
|
||||
if ttl > 0 {
|
||||
go k.janitor(ctx)
|
||||
go limiter.janitor(ctx)
|
||||
}
|
||||
return k
|
||||
return limiter
|
||||
}
|
||||
|
||||
func (k *KeyedLimiter) shard(key string) *limiterShard {
|
||||
@@ -99,45 +112,47 @@ func (k *KeyedLimiter) shard(key string) *limiterShard {
|
||||
// Allow implements Limiter.
|
||||
func (k *KeyedLimiter) Allow(key string) (bool, time.Duration) {
|
||||
now := time.Now()
|
||||
s := k.shard(key)
|
||||
s.mu.Lock()
|
||||
e := s.entries[key]
|
||||
if e == nil {
|
||||
e = &limiterEntry{lim: rate.NewLimiter(k.limit, k.burst)}
|
||||
s.entries[key] = e
|
||||
shard := k.shard(key)
|
||||
shard.mu.Lock()
|
||||
entry := shard.entries[key]
|
||||
if entry == nil {
|
||||
entry = &limiterEntry{
|
||||
limiter: rate.NewLimiter(k.limit, k.burst),
|
||||
}
|
||||
shard.entries[key] = entry
|
||||
}
|
||||
e.last = now
|
||||
lim := e.lim
|
||||
s.mu.Unlock()
|
||||
entry.last = now
|
||||
limiter := entry.limiter
|
||||
shard.mu.Unlock()
|
||||
|
||||
res := lim.ReserveN(now, 1)
|
||||
res := limiter.ReserveN(now, 1)
|
||||
if !res.OK() {
|
||||
return false, time.Second
|
||||
}
|
||||
if d := res.DelayFrom(now); d > 0 {
|
||||
if delay := res.DelayFrom(now); delay > 0 {
|
||||
res.CancelAt(now)
|
||||
return false, d
|
||||
return false, delay
|
||||
}
|
||||
return true, 0
|
||||
}
|
||||
|
||||
func (k *KeyedLimiter) janitor(ctx context.Context) {
|
||||
t := time.NewTicker(k.ttl / 2)
|
||||
defer t.Stop()
|
||||
ticker := time.NewTicker(k.ttl / 2)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case now := <-t.C:
|
||||
case now := <-ticker.C:
|
||||
for i := range k.shards {
|
||||
s := &k.shards[i]
|
||||
s.mu.Lock()
|
||||
for key, e := range s.entries {
|
||||
if now.Sub(e.last) > k.ttl {
|
||||
delete(s.entries, key)
|
||||
shard := &k.shards[i]
|
||||
shard.mu.Lock()
|
||||
for key, entry := range shard.entries {
|
||||
if now.Sub(entry.last) > k.ttl {
|
||||
delete(shard.entries, key)
|
||||
}
|
||||
}
|
||||
s.mu.Unlock()
|
||||
shard.mu.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,8 @@ import (
|
||||
|
||||
func TestRateLimit(t *testing.T) {
|
||||
l := NewKeyedLimiter(t.Context(), 1, 2, 0)
|
||||
h := RateLimit(l, nil)(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {}))
|
||||
h := RateLimit(l, nil)(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
|
||||
}))
|
||||
var last int
|
||||
for i := 0; i < 4; i++ {
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
@@ -20,8 +20,8 @@ type ClientInfo struct {
|
||||
// Client returns the ClientInfo stored by RealIP. Without RealIP it falls back
|
||||
// to the connection itself (see peerInfo) and ignores forwarding headers.
|
||||
func Client(r *http.Request) ClientInfo {
|
||||
if ci, ok := r.Context().Value(clientInfoKey).(ClientInfo); ok {
|
||||
return ci
|
||||
if info, ok := r.Context().Value(clientInfoKey).(ClientInfo); ok {
|
||||
return info
|
||||
}
|
||||
return peerInfo(r)
|
||||
}
|
||||
@@ -62,10 +62,14 @@ func peerInfo(r *http.Request) ClientInfo {
|
||||
|
||||
// RealIPOptions configures RealIP.
|
||||
type RealIPOptions struct {
|
||||
// Trusted are the proxies whose forwarding headers are believed.
|
||||
Trusted []netip.Prefix
|
||||
// Headers is processed in increasing priority order - the last header in the
|
||||
// list that is found is the header used to determine a client IP address.
|
||||
Headers []string // e.g. X-Forwarded-For, Forwarded, X-Real-IP
|
||||
// Headers are the headers the client IP is read from, in priority order:
|
||||
// the first of them that the request has and that gives an address is
|
||||
// used. E.g. X-Forwarded-For, Forwarded, X-Real-IP.
|
||||
Headers []string
|
||||
// StripUntrusted removes the forwarding headers from requests that are
|
||||
// not from a trusted proxy.
|
||||
StripUntrusted bool
|
||||
}
|
||||
|
||||
@@ -91,9 +95,9 @@ var forwardingHeaders = []string{
|
||||
// PROXY protocol is handled at the listener (see server package), so by the
|
||||
// time this runs r.RemoteAddr already holds the address from the PROXY header.
|
||||
func RealIP(opts RealIPOptions) func(http.Handler) http.Handler {
|
||||
trusted := func(a netip.Addr) bool {
|
||||
for _, p := range opts.Trusted {
|
||||
if p.Contains(a) {
|
||||
trusted := func(addr netip.Addr) bool {
|
||||
for _, prefix := range opts.Trusted {
|
||||
if prefix.Contains(addr) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -105,19 +109,19 @@ func RealIP(opts RealIPOptions) func(http.Handler) http.Handler {
|
||||
info := peerInfo(r)
|
||||
|
||||
if info.Peer.IsValid() && trusted(info.Peer) {
|
||||
for _, h := range opts.Headers {
|
||||
if ip, ok := clientFromHeader(r.Header, h, trusted); ok {
|
||||
for _, name := range opts.Headers {
|
||||
if ip, ok := clientFromHeader(r.Header, name, trusted); ok {
|
||||
info.IP = ip
|
||||
info.ViaProxy = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if p := forwardedProto(r.Header); p == "http" || p == "https" {
|
||||
info.Scheme = p
|
||||
if proto := forwardedProto(r.Header); proto == "http" || proto == "https" {
|
||||
info.Scheme = proto
|
||||
}
|
||||
} else if opts.StripUntrusted {
|
||||
for _, h := range forwardingHeaders {
|
||||
r.Header.Del(h)
|
||||
for _, name := range forwardingHeaders {
|
||||
r.Header.Del(name)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,17 +132,17 @@ func RealIP(opts RealIPOptions) func(http.Handler) http.Handler {
|
||||
}
|
||||
|
||||
func clientFromHeader(
|
||||
h http.Header, name string, trusted func(netip.Addr) bool,
|
||||
header http.Header, name string, trusted func(netip.Addr) bool,
|
||||
) (netip.Addr, bool) {
|
||||
switch http.CanonicalHeaderKey(name) {
|
||||
case "X-Forwarded-For":
|
||||
return walkChain(splitList(h.Values("X-Forwarded-For")), trusted)
|
||||
return walkChain(splitList(header.Values("X-Forwarded-For")), trusted)
|
||||
|
||||
case "Forwarded":
|
||||
var chain []string
|
||||
for _, el := range splitList(h.Values("Forwarded")) {
|
||||
if v, ok := forwardedParam(el, "for"); ok {
|
||||
chain = append(chain, v)
|
||||
for _, element := range splitList(header.Values("Forwarded")) {
|
||||
if value, ok := forwardedParam(element, "for"); ok {
|
||||
chain = append(chain, value)
|
||||
} else {
|
||||
chain = append(chain, "") // element without for= breaks the chain
|
||||
}
|
||||
@@ -146,8 +150,8 @@ func clientFromHeader(
|
||||
return walkChain(chain, trusted)
|
||||
|
||||
case "X-Real-Ip":
|
||||
if v := strings.TrimSpace(h.Get("X-Real-IP")); v != "" {
|
||||
return parseIPToken(v)
|
||||
if value := strings.TrimSpace(header.Get("X-Real-IP")); value != "" {
|
||||
return parseIPToken(value)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,15 +178,15 @@ func walkChain(
|
||||
return last, last.IsValid()
|
||||
}
|
||||
|
||||
func forwardedProto(h http.Header) string {
|
||||
if v := h.Get("X-Forwarded-Proto"); v != "" {
|
||||
first, _, _ := strings.Cut(v, ",")
|
||||
func forwardedProto(header http.Header) string {
|
||||
if value := header.Get("X-Forwarded-Proto"); value != "" {
|
||||
first, _, _ := strings.Cut(value, ",")
|
||||
return strings.ToLower(strings.TrimSpace(first))
|
||||
}
|
||||
|
||||
if els := splitList(h.Values("Forwarded")); len(els) > 0 {
|
||||
if v, ok := forwardedParam(els[0], "proto"); ok {
|
||||
return strings.ToLower(v)
|
||||
if elements := splitList(header.Values("Forwarded")); len(elements) > 0 {
|
||||
if value, ok := forwardedParam(elements[0], "proto"); ok {
|
||||
return strings.ToLower(value)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,9 +197,9 @@ func forwardedProto(h http.Header) string {
|
||||
// element (RFC 7239).
|
||||
func forwardedParam(element, key string) (string, bool) {
|
||||
for _, pair := range strings.Split(element, ";") {
|
||||
k, v, ok := strings.Cut(strings.TrimSpace(pair), "=")
|
||||
if ok && strings.EqualFold(k, key) {
|
||||
return strings.Trim(strings.TrimSpace(v), `"`), true
|
||||
name, value, ok := strings.Cut(strings.TrimSpace(pair), "=")
|
||||
if ok && strings.EqualFold(name, key) {
|
||||
return strings.Trim(strings.TrimSpace(value), `"`), true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
@@ -203,8 +207,8 @@ func forwardedParam(element, key string) (string, bool) {
|
||||
|
||||
func splitList(values []string) []string {
|
||||
var out []string
|
||||
for _, v := range values {
|
||||
for _, part := range strings.Split(v, ",") {
|
||||
for _, value := range values {
|
||||
for _, part := range strings.Split(value, ",") {
|
||||
if part = strings.TrimSpace(part); part != "" {
|
||||
out = append(out, part)
|
||||
}
|
||||
@@ -214,23 +218,23 @@ func splitList(values []string) []string {
|
||||
}
|
||||
|
||||
// parseIPToken accepts "1.2.3.4", "1.2.3.4:80", "::1", "[::1]" and "[::1]:80".
|
||||
func parseIPToken(s string) (netip.Addr, bool) {
|
||||
s = strings.Trim(strings.TrimSpace(s), `"`)
|
||||
if ap, err := netip.ParseAddrPort(s); err == nil {
|
||||
return ap.Addr().Unmap(), true
|
||||
func parseIPToken(token string) (netip.Addr, bool) {
|
||||
token = strings.Trim(strings.TrimSpace(token), `"`)
|
||||
if addrPort, err := netip.ParseAddrPort(token); err == nil {
|
||||
return addrPort.Addr().Unmap(), true
|
||||
}
|
||||
|
||||
s = strings.TrimSuffix(strings.TrimPrefix(s, "["), "]")
|
||||
if a, err := netip.ParseAddr(s); err == nil {
|
||||
return a.Unmap().WithZone(""), true
|
||||
token = strings.TrimSuffix(strings.TrimPrefix(token, "["), "]")
|
||||
if addr, err := netip.ParseAddr(token); err == nil {
|
||||
return addr.Unmap().WithZone(""), true
|
||||
}
|
||||
|
||||
return netip.Addr{}, false
|
||||
}
|
||||
|
||||
func addrFromRemote(remote string) netip.Addr {
|
||||
if a, ok := parseIPToken(remote); ok {
|
||||
return a
|
||||
if addr, ok := parseIPToken(remote); ok {
|
||||
return addr
|
||||
}
|
||||
return netip.Addr{}
|
||||
}
|
||||
@@ -12,7 +12,9 @@ import (
|
||||
func serveRealIP(t *testing.T, opts RealIPOptions, peer string, h map[string]string) ClientInfo {
|
||||
t.Helper()
|
||||
var got ClientInfo
|
||||
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { got = Client(r) })
|
||||
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
got = Client(r)
|
||||
})
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
req.RemoteAddr = peer
|
||||
for k, v := range h {
|
||||
|
||||
Reference in new issue
Block a user