All checks were successful
Build and Publish Docker Image / deploy (push) Successful in 1m41s
- Implement storage backend deletion, which automatically resets default storage settings and user-specific overrides when a backend is removed. - Add unit tests covering the delete action and its cleanup side effects. - Improve admin UI responsiveness, fixing table scrolling, flex wrapping, and text truncation for long storage backend names. - Update security documentation to clarify trusted proxy configurations and explain how trusted proxies are protected from automatic bans.
141 lines
3.3 KiB
Go
141 lines
3.3 KiB
Go
package services
|
|
|
|
import (
|
|
"context"
|
|
"net"
|
|
"net/http"
|
|
"strings"
|
|
)
|
|
|
|
type clientIPContextKey struct{}
|
|
|
|
func WithClientIP(r *http.Request, ip string) *http.Request {
|
|
return r.WithContext(context.WithValue(r.Context(), clientIPContextKey{}, ip))
|
|
}
|
|
|
|
func ClientIPFromContext(r *http.Request) (string, bool) {
|
|
ip, ok := r.Context().Value(clientIPContextKey{}).(string)
|
|
return ip, ok && ip != ""
|
|
}
|
|
|
|
// ClientIP resolves the effective client IP. When trustedProxies is empty,
|
|
// forwarded headers are trusted for easy reverse-proxy/container defaults.
|
|
func ClientIP(remoteAddr, forwardedFor, realIP string, trustedProxies []string) string {
|
|
remoteIP := IPOnly(remoteAddr)
|
|
if len(trustedProxies) == 0 || remoteTrusted(remoteIP, trustedProxies) {
|
|
if ip := firstForwardedIP(forwardedFor); ip != "" {
|
|
return IPOnly(ip)
|
|
}
|
|
if ip := strings.TrimSpace(realIP); ip != "" {
|
|
return IPOnly(ip)
|
|
}
|
|
}
|
|
return remoteIP
|
|
}
|
|
|
|
func IPOnly(remoteAddr string) string {
|
|
host := strings.TrimSpace(remoteAddr)
|
|
if splitHost, _, err := net.SplitHostPort(remoteAddr); err == nil {
|
|
host = splitHost
|
|
}
|
|
return strings.Trim(host, "[]")
|
|
}
|
|
|
|
func IsProtectedProxyIP(ip string, trustedProxies []string) bool {
|
|
parsed := net.ParseIP(IPOnly(ip))
|
|
if parsed == nil {
|
|
return false
|
|
}
|
|
if parsed.IsLoopback() {
|
|
return true
|
|
}
|
|
return remoteTrusted(parsed.String(), trustedProxies)
|
|
}
|
|
|
|
func ProtectedBanTarget(target string, trustedProxies []string) bool {
|
|
normalized, err := NormalizeBanTarget(target)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
if !strings.Contains(normalized, "/") {
|
|
return IsProtectedProxyIP(normalized, trustedProxies)
|
|
}
|
|
_, targetNet, err := net.ParseCIDR(normalized)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
if targetNet.Contains(net.ParseIP("127.0.0.1")) || targetNet.Contains(net.ParseIP("::1")) {
|
|
return true
|
|
}
|
|
for _, trusted := range trustedProxies {
|
|
trusted = strings.TrimSpace(trusted)
|
|
if trusted == "" {
|
|
continue
|
|
}
|
|
if strings.Contains(trusted, "/") {
|
|
if _, trustedNet, err := net.ParseCIDR(trusted); err == nil && networksOverlap(targetNet, trustedNet) {
|
|
return true
|
|
}
|
|
continue
|
|
}
|
|
if ip := net.ParseIP(IPOnly(trusted)); ip != nil && targetNet.Contains(ip) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func firstForwardedIP(forwardedFor string) string {
|
|
var fallback string
|
|
for _, part := range strings.Split(forwardedFor, ",") {
|
|
ip := IPOnly(part)
|
|
if net.ParseIP(ip) == nil {
|
|
continue
|
|
}
|
|
if fallback == "" {
|
|
fallback = ip
|
|
}
|
|
if isExternalIP(ip) {
|
|
return ip
|
|
}
|
|
}
|
|
return fallback
|
|
}
|
|
|
|
func remoteTrusted(remoteIP string, trustedProxies []string) bool {
|
|
parsed := net.ParseIP(remoteIP)
|
|
if parsed == nil {
|
|
return false
|
|
}
|
|
for _, trusted := range trustedProxies {
|
|
trusted = strings.TrimSpace(trusted)
|
|
if trusted == "" {
|
|
continue
|
|
}
|
|
if strings.Contains(trusted, "/") {
|
|
if _, network, err := net.ParseCIDR(trusted); err == nil && network.Contains(parsed) {
|
|
return true
|
|
}
|
|
continue
|
|
}
|
|
if ip := net.ParseIP(trusted); ip != nil && ip.Equal(parsed) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func isExternalIP(ip string) bool {
|
|
parsed := net.ParseIP(IPOnly(ip))
|
|
return parsed != nil &&
|
|
!parsed.IsLoopback() &&
|
|
!parsed.IsPrivate() &&
|
|
!parsed.IsLinkLocalUnicast() &&
|
|
!parsed.IsLinkLocalMulticast() &&
|
|
!parsed.IsUnspecified()
|
|
}
|
|
|
|
func networksOverlap(a, b *net.IPNet) bool {
|
|
return a.Contains(b.IP) || b.Contains(a.IP)
|
|
}
|