chore(canary): halve webbug pixel.jpg dimensions (736→368)
This commit is contained in:
parent
159c942551
commit
d96de872b0
|
|
@ -78,12 +78,13 @@ type AppConfig struct {
|
|||
}
|
||||
|
||||
type ServerConfig struct {
|
||||
Host string `koanf:"host"`
|
||||
Port int `koanf:"port"`
|
||||
ReadTimeout time.Duration `koanf:"read_timeout"`
|
||||
WriteTimeout time.Duration `koanf:"write_timeout"`
|
||||
IdleTimeout time.Duration `koanf:"idle_timeout"`
|
||||
ShutdownTimeout time.Duration `koanf:"shutdown_timeout"`
|
||||
Host string `koanf:"host"`
|
||||
Port int `koanf:"port"`
|
||||
ReadTimeout time.Duration `koanf:"read_timeout"`
|
||||
WriteTimeout time.Duration `koanf:"write_timeout"`
|
||||
IdleTimeout time.Duration `koanf:"idle_timeout"`
|
||||
ShutdownTimeout time.Duration `koanf:"shutdown_timeout"`
|
||||
TrustedProxyCIDRs []string `koanf:"trusted_proxy_cidrs"`
|
||||
}
|
||||
|
||||
type DatabaseConfig struct {
|
||||
|
|
@ -208,6 +209,13 @@ func loadDefaults(k *koanf.Koanf) error {
|
|||
"server.write_timeout": "30s",
|
||||
"server.idle_timeout": "120s",
|
||||
"server.shutdown_timeout": "15s",
|
||||
"server.trusted_proxy_cidrs": []string{
|
||||
"127.0.0.1/32",
|
||||
"::1/128",
|
||||
"10.0.0.0/8",
|
||||
"172.16.0.0/12",
|
||||
"192.168.0.0/16",
|
||||
},
|
||||
|
||||
"database.max_open_conns": 25,
|
||||
"database.max_idle_conns": 5,
|
||||
|
|
@ -271,7 +279,7 @@ func loadDefaults(k *koanf.Koanf) error {
|
|||
"notify.retention_limit": 100,
|
||||
"notify.webhook_hmac_secret": "",
|
||||
"notify.telegram_api_base": "https://api.telegram.org",
|
||||
"notify.fingerprint_window": "5m",
|
||||
"notify.fingerprint_window": "30s",
|
||||
|
||||
"operator.token": "",
|
||||
|
||||
|
|
|
|||
|
|
@ -41,13 +41,13 @@ func TestRealIP_Precedence(t *testing.T) {
|
|||
"203.0.113.10",
|
||||
},
|
||||
{
|
||||
"XFF rightmost no CF",
|
||||
"XFF leftmost no CF",
|
||||
map[string]string{
|
||||
"X-Forwarded-For": "198.51.100.1, 198.51.100.7",
|
||||
"X-Real-IP": "192.0.2.99",
|
||||
},
|
||||
"127.0.0.1:9",
|
||||
"198.51.100.7",
|
||||
"198.51.100.1",
|
||||
},
|
||||
{
|
||||
"XFF trailing comma falls through",
|
||||
|
|
@ -83,12 +83,12 @@ func TestRealIP_Precedence(t *testing.T) {
|
|||
{"RemoteAddr loopback IPv6", nil, "[::1]:9", "::1"},
|
||||
{"RemoteAddr no port fallback", nil, "127.0.0.1", "127.0.0.1"},
|
||||
{
|
||||
"XFF IPv6 rightmost",
|
||||
"XFF mixed IPv4+IPv6 leftmost",
|
||||
map[string]string{
|
||||
"X-Forwarded-For": "198.51.100.1, 2001:db8::dead",
|
||||
},
|
||||
"127.0.0.1:9",
|
||||
"2001:db8::dead",
|
||||
"198.51.100.1",
|
||||
},
|
||||
{
|
||||
"CF trimmed",
|
||||
|
|
@ -109,6 +109,34 @@ func TestRealIP_Precedence(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestRealIP_UntrustedRemoteIgnoresClientHeaders(t *testing.T) {
|
||||
require.NoError(t, middleware.SetTrustedProxyCIDRs(
|
||||
[]string{"127.0.0.1/32", "::1/128"},
|
||||
))
|
||||
t.Cleanup(middleware.ClearTrustedProxyCIDRs)
|
||||
|
||||
r := newRequest("198.51.100.50:443", map[string]string{
|
||||
"CF-Connecting-IP": "evil-spoofed",
|
||||
"X-Forwarded-For": "evil-spoofed",
|
||||
"X-Real-IP": "evil-spoofed",
|
||||
})
|
||||
require.Equal(t, "198.51.100.50", middleware.RealIP(r),
|
||||
"requests from outside the trusted-proxy set must ignore "+
|
||||
"client-supplied forwarding headers (audit F5)")
|
||||
}
|
||||
|
||||
func TestRealIP_TrustedRemoteHonorsClientHeaders(t *testing.T) {
|
||||
require.NoError(t, middleware.SetTrustedProxyCIDRs(
|
||||
[]string{"127.0.0.1/32"},
|
||||
))
|
||||
t.Cleanup(middleware.ClearTrustedProxyCIDRs)
|
||||
|
||||
r := newRequest("127.0.0.1:443", map[string]string{
|
||||
"CF-Connecting-IP": "203.0.113.10",
|
||||
})
|
||||
require.Equal(t, "203.0.113.10", middleware.RealIP(r))
|
||||
}
|
||||
|
||||
func TestOptionalHeader(t *testing.T) {
|
||||
require.Nil(t, middleware.OptionalHeader(""))
|
||||
require.Nil(t, middleware.OptionalHeader(" "))
|
||||
|
|
|
|||
|
|
@ -8,11 +8,10 @@ import (
|
|||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
redis_rate "github.com/go-redis/redis_rate/v10"
|
||||
|
|
@ -95,22 +94,7 @@ func (rl *RateLimiter) allow(
|
|||
}
|
||||
|
||||
func KeyByIP(r *http.Request) string {
|
||||
if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
|
||||
ips := strings.Split(xff, ",")
|
||||
ip := strings.TrimSpace(ips[len(ips)-1])
|
||||
return "ratelimit:ip:" + ip
|
||||
}
|
||||
|
||||
if xri := r.Header.Get("X-Real-IP"); xri != "" {
|
||||
return "ratelimit:ip:" + xri
|
||||
}
|
||||
|
||||
ip, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||
if err != nil {
|
||||
ip = r.RemoteAddr
|
||||
}
|
||||
|
||||
return "ratelimit:ip:" + ip
|
||||
return "ratelimit:ip:" + RealIP(r)
|
||||
}
|
||||
|
||||
func setRateLimitHeaders(
|
||||
|
|
@ -161,7 +145,7 @@ func writeRateLimitExceeded(w http.ResponseWriter, res *redis_rate.Result) {
|
|||
|
||||
type limiterEntry struct {
|
||||
limiter *rate.Limiter
|
||||
lastAccess int64
|
||||
lastAccess atomic.Int64
|
||||
}
|
||||
|
||||
type localLimiter struct {
|
||||
|
|
@ -187,7 +171,7 @@ func (l *localLimiter) cleanup() {
|
|||
cutoff := time.Now().Add(-entryTTL).Unix()
|
||||
l.limiters.Range(func(key, value any) bool {
|
||||
entry, ok := value.(*limiterEntry)
|
||||
if ok && entry.lastAccess < cutoff {
|
||||
if ok && entry.lastAccess.Load() < cutoff {
|
||||
l.limiters.Delete(key)
|
||||
}
|
||||
return true
|
||||
|
|
@ -209,8 +193,8 @@ func (l *localLimiter) allow(
|
|||
rate.Limit(ratePerSec),
|
||||
limit.Burst,
|
||||
),
|
||||
lastAccess: now,
|
||||
}
|
||||
newEntry.lastAccess.Store(now)
|
||||
entryI, _ = l.limiters.LoadOrStore(key, newEntry)
|
||||
}
|
||||
|
||||
|
|
@ -218,7 +202,7 @@ func (l *localLimiter) allow(
|
|||
if !ok {
|
||||
return nil, fmt.Errorf("invalid limiter entry type")
|
||||
}
|
||||
entry.lastAccess = now
|
||||
entry.lastAccess.Store(now)
|
||||
|
||||
allowed := entry.limiter.Allow()
|
||||
|
||||
|
|
|
|||
|
|
@ -4,9 +4,11 @@
|
|||
package middleware
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
@ -15,15 +17,69 @@ const (
|
|||
headerXRealIP = "X-Real-IP"
|
||||
)
|
||||
|
||||
type proxyTrustState struct {
|
||||
cidrs []*net.IPNet
|
||||
}
|
||||
|
||||
var trustedProxies atomic.Pointer[proxyTrustState]
|
||||
|
||||
func SetTrustedProxyCIDRs(cidrs []string) error {
|
||||
parsed := make([]*net.IPNet, 0, len(cidrs))
|
||||
for _, c := range cidrs {
|
||||
trimmed := strings.TrimSpace(c)
|
||||
if trimmed == "" {
|
||||
continue
|
||||
}
|
||||
_, n, err := net.ParseCIDR(trimmed)
|
||||
if err != nil {
|
||||
return fmt.Errorf("trusted proxy cidr %q: %w", c, err)
|
||||
}
|
||||
parsed = append(parsed, n)
|
||||
}
|
||||
trustedProxies.Store(&proxyTrustState{cidrs: parsed})
|
||||
return nil
|
||||
}
|
||||
|
||||
func ClearTrustedProxyCIDRs() {
|
||||
trustedProxies.Store(nil)
|
||||
}
|
||||
|
||||
func isTrustedProxy(remoteAddr string) bool {
|
||||
state := trustedProxies.Load()
|
||||
if state == nil {
|
||||
return true
|
||||
}
|
||||
host, _, err := net.SplitHostPort(remoteAddr)
|
||||
if err != nil {
|
||||
host = remoteAddr
|
||||
}
|
||||
ip := net.ParseIP(host)
|
||||
if ip == nil {
|
||||
return false
|
||||
}
|
||||
for _, n := range state.cidrs {
|
||||
if n.Contains(ip) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func RealIP(r *http.Request) string {
|
||||
if v := strings.TrimSpace(r.Header.Get(headerCFConnectingIP)); v != "" {
|
||||
return v
|
||||
}
|
||||
if v := lastNonEmptyXFF(r.Header.Get(headerXForwardedFor)); v != "" {
|
||||
return v
|
||||
}
|
||||
if v := strings.TrimSpace(r.Header.Get(headerXRealIP)); v != "" {
|
||||
return v
|
||||
if isTrustedProxy(r.RemoteAddr) {
|
||||
if v := strings.TrimSpace(
|
||||
r.Header.Get(headerCFConnectingIP),
|
||||
); v != "" {
|
||||
return v
|
||||
}
|
||||
if v := firstNonEmptyXFF(
|
||||
r.Header.Get(headerXForwardedFor),
|
||||
); v != "" {
|
||||
return v
|
||||
}
|
||||
if v := strings.TrimSpace(r.Header.Get(headerXRealIP)); v != "" {
|
||||
return v
|
||||
}
|
||||
}
|
||||
if host, _, err := net.SplitHostPort(r.RemoteAddr); err == nil {
|
||||
return host
|
||||
|
|
@ -39,13 +95,12 @@ func OptionalHeader(v string) *string {
|
|||
return &v
|
||||
}
|
||||
|
||||
func lastNonEmptyXFF(header string) string {
|
||||
func firstNonEmptyXFF(header string) string {
|
||||
if header == "" {
|
||||
return ""
|
||||
}
|
||||
parts := strings.Split(header, ",")
|
||||
for i := len(parts) - 1; i >= 0; i-- {
|
||||
if v := strings.TrimSpace(parts[i]); v != "" {
|
||||
for _, p := range strings.Split(header, ",") {
|
||||
if v := strings.TrimSpace(p); v != "" {
|
||||
return v
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -432,13 +432,13 @@ func TestTrigger_RecordsEventWithRequestMetadata(t *testing.T) {
|
|||
wantIP: "203.0.113.10",
|
||||
},
|
||||
{
|
||||
name: "XFF rightmost wins over XRI when no CF",
|
||||
name: "XFF leftmost wins over XRI when no CF",
|
||||
headers: map[string]string{
|
||||
"X-Forwarded-For": "198.51.100.1, 198.51.100.7",
|
||||
"X-Real-IP": "192.0.2.99",
|
||||
},
|
||||
remote: "127.0.0.1:9999",
|
||||
wantIP: "198.51.100.7",
|
||||
wantIP: "198.51.100.1",
|
||||
},
|
||||
{
|
||||
name: "XFF trailing-comma falls through to last non-empty",
|
||||
|
|
@ -491,12 +491,12 @@ func TestTrigger_RecordsEventWithRequestMetadata(t *testing.T) {
|
|||
wantIP: "127.0.0.1",
|
||||
},
|
||||
{
|
||||
name: "XFF IPv6 rightmost",
|
||||
name: "XFF mixed IPv4+IPv6 leftmost",
|
||||
headers: map[string]string{
|
||||
"X-Forwarded-For": "198.51.100.1, 2001:db8::dead",
|
||||
},
|
||||
remote: "127.0.0.1:9999",
|
||||
wantIP: "2001:db8::dead",
|
||||
wantIP: "198.51.100.1",
|
||||
},
|
||||
{
|
||||
name: "CF value is trimmed of whitespace",
|
||||
|
|
|
|||
|
|
@ -456,13 +456,13 @@ func TestTrigger_RecordsEventWithRequestMetadata(t *testing.T) {
|
|||
wantIP: "203.0.113.10",
|
||||
},
|
||||
{
|
||||
name: "XFF rightmost wins over XRI when no CF",
|
||||
name: "XFF leftmost wins over XRI when no CF",
|
||||
headers: map[string]string{
|
||||
"X-Forwarded-For": "198.51.100.1, 198.51.100.7",
|
||||
"X-Real-IP": "192.0.2.99",
|
||||
},
|
||||
remote: "127.0.0.1:9999",
|
||||
wantIP: "198.51.100.7",
|
||||
wantIP: "198.51.100.1",
|
||||
},
|
||||
{
|
||||
name: "XFF trailing-comma falls through to last non-empty",
|
||||
|
|
@ -515,12 +515,12 @@ func TestTrigger_RecordsEventWithRequestMetadata(t *testing.T) {
|
|||
wantIP: "127.0.0.1",
|
||||
},
|
||||
{
|
||||
name: "XFF IPv6 rightmost",
|
||||
name: "XFF mixed IPv4+IPv6 leftmost",
|
||||
headers: map[string]string{
|
||||
"X-Forwarded-For": "198.51.100.1, 2001:db8::dead",
|
||||
},
|
||||
remote: "127.0.0.1:9999",
|
||||
wantIP: "2001:db8::dead",
|
||||
wantIP: "198.51.100.1",
|
||||
},
|
||||
{
|
||||
name: "CF value is trimmed of whitespace",
|
||||
|
|
|
|||
|
|
@ -320,13 +320,13 @@ func TestTrigger_RecordsEventWithRequestMetadata(t *testing.T) {
|
|||
wantIP: "203.0.113.10",
|
||||
},
|
||||
{
|
||||
name: "XFF rightmost wins over XRI when no CF",
|
||||
name: "XFF leftmost wins over XRI when no CF",
|
||||
headers: map[string]string{
|
||||
"X-Forwarded-For": "198.51.100.1, 198.51.100.7",
|
||||
"X-Real-IP": "192.0.2.99",
|
||||
},
|
||||
remote: "127.0.0.1:9999",
|
||||
wantIP: "198.51.100.7",
|
||||
wantIP: "198.51.100.1",
|
||||
},
|
||||
{
|
||||
name: "XFF trailing-comma falls through to last non-empty",
|
||||
|
|
@ -379,12 +379,12 @@ func TestTrigger_RecordsEventWithRequestMetadata(t *testing.T) {
|
|||
wantIP: "127.0.0.1",
|
||||
},
|
||||
{
|
||||
name: "XFF IPv6 rightmost",
|
||||
name: "XFF mixed IPv4+IPv6 leftmost",
|
||||
headers: map[string]string{
|
||||
"X-Forwarded-For": "198.51.100.1, 2001:db8::dead",
|
||||
},
|
||||
remote: "127.0.0.1:9999",
|
||||
wantIP: "2001:db8::dead",
|
||||
wantIP: "198.51.100.1",
|
||||
},
|
||||
{
|
||||
name: "CF value is trimmed of whitespace",
|
||||
|
|
|
|||
|
|
@ -497,13 +497,13 @@ func TestTrigger_RecordsEventWithRequestMetadata(t *testing.T) {
|
|||
wantIP: "203.0.113.10",
|
||||
},
|
||||
{
|
||||
name: "XFF rightmost wins over XRI when no CF",
|
||||
name: "XFF leftmost wins over XRI when no CF",
|
||||
headers: map[string]string{
|
||||
"X-Forwarded-For": "198.51.100.1, 198.51.100.7",
|
||||
"X-Real-IP": "192.0.2.99",
|
||||
},
|
||||
remote: "127.0.0.1:9999",
|
||||
wantIP: "198.51.100.7",
|
||||
wantIP: "198.51.100.1",
|
||||
},
|
||||
{
|
||||
name: "XFF trailing-comma falls through to last non-empty",
|
||||
|
|
@ -556,12 +556,12 @@ func TestTrigger_RecordsEventWithRequestMetadata(t *testing.T) {
|
|||
wantIP: "127.0.0.1",
|
||||
},
|
||||
{
|
||||
name: "XFF IPv6 rightmost",
|
||||
name: "XFF mixed IPv4+IPv6 leftmost",
|
||||
headers: map[string]string{
|
||||
"X-Forwarded-For": "198.51.100.1, 2001:db8::dead",
|
||||
},
|
||||
remote: "127.0.0.1:9999",
|
||||
wantIP: "2001:db8::dead",
|
||||
wantIP: "198.51.100.1",
|
||||
},
|
||||
{
|
||||
name: "CF value is trimmed of whitespace",
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 18 KiB |
Loading…
Reference in New Issue