diff --git a/internals/config/structure/structure.go b/internals/config/structure/structure.go index 3b39cab0..2052c11a 100644 --- a/internals/config/structure/structure.go +++ b/internals/config/structure/structure.go @@ -49,6 +49,9 @@ type ACCESS struct { ENDPOINTS []string `koanf:"endpoints"` FIELD_POLICIES map[string]FieldPolicy `koanf:"fieldpolicies" childtransform:"default"` RATE_LIMITING RateLimiting `koanf:"ratelimiting"` + IP_FILTER []string `koanf:"ipfilter"` + TRUSTED_IPS []string `koanf:"trustedips"` + TRUSTED_PROXIES []string `koanf:"trustedproxies"` } type FieldPolicy struct { diff --git a/internals/proxy/middlewares/auth.go b/internals/proxy/middlewares/auth.go index 49518ebd..5946f27f 100644 --- a/internals/proxy/middlewares/auth.go +++ b/internals/proxy/middlewares/auth.go @@ -274,7 +274,7 @@ func (chain *AuthChain) Eval(w http.ResponseWriter, req *http.Request, tokens [] token, err = method.Authenticate(w, req, tokens) if err != nil { - logger.Warn("User failed ", method.Name, " auth: ", err.Error()) + logger.Warn("Client failed ", method.Name, " auth: ", err.Error()) } if token != "" { diff --git a/internals/proxy/middlewares/clientip.go b/internals/proxy/middlewares/clientip.go new file mode 100644 index 00000000..11126c2a --- /dev/null +++ b/internals/proxy/middlewares/clientip.go @@ -0,0 +1,40 @@ +package middlewares + +import ( + "net" + "net/http" +) + +var InternalClientIP Middleware = Middleware{ + Name: "_Client_IP", + Use: clientIPHandler, +} + +var trustedClientKey contextKey = "isClientTrusted" + +func clientIPHandler(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + logger := getLogger(req) + + conf := getConfigByReq(req) + + rawTrustedIPs := conf.SETTINGS.ACCESS.TRUSTED_IPS + + if rawTrustedIPs == nil { + rawTrustedIPs = getConfig("").SETTINGS.ACCESS.TRUSTED_IPS + } + + ip := getContext[net.IP](req, clientIPKey) + + trustedIPs := parseIPsAndIPNets(rawTrustedIPs) + trusted := isIPInList(ip, trustedIPs) + + if trusted { + logger.Dev("Connection from trusted Client: ", ip.String()) + } + + req = setContext(req, trustedClientKey, trusted) + + next.ServeHTTP(w, req) + }) +} \ No newline at end of file diff --git a/internals/proxy/middlewares/endpoints.go b/internals/proxy/middlewares/endpoints.go index bd34c228..c33e45be 100644 --- a/internals/proxy/middlewares/endpoints.go +++ b/internals/proxy/middlewares/endpoints.go @@ -26,8 +26,8 @@ func endpointsHandler(next http.Handler) http.Handler { reqPath := req.URL.Path - if isBlocked(reqPath, endpoints) { - logger.Warn("User tried to access blocked endpoint: ", reqPath) + if isEndpointBlocked(reqPath, endpoints) { + logger.Warn("Client tried to access blocked endpoint: ", reqPath) http.Error(w, "Forbidden", http.StatusForbidden) return } @@ -58,8 +58,8 @@ func matchesPattern(endpoint, pattern string) bool { return ok } -func isBlocked(endpoint string, endpoints []string) bool { - if len(endpoints) == 0 { +func isEndpointBlocked(endpoint string, endpoints []string) bool { + if len(endpoints) == 0 || endpoints == nil { // default: allow all return false } diff --git a/internals/proxy/middlewares/ipfilter.go b/internals/proxy/middlewares/ipfilter.go new file mode 100644 index 00000000..b4138e47 --- /dev/null +++ b/internals/proxy/middlewares/ipfilter.go @@ -0,0 +1,95 @@ +package middlewares + +import ( + "net" + "net/http" + "slices" + "strings" +) + +var IPFilter Middleware = Middleware{ + Name: "IP Filter", + Use: ipFilterHandler, +} + +func ipFilterHandler(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + logger := getLogger(req) + + conf := getConfigByReq(req) + + ipFilter := conf.SETTINGS.ACCESS.IP_FILTER + + if ipFilter == nil { + ipFilter = getConfig("").SETTINGS.ACCESS.ENDPOINTS + } + + ip := getContext[net.IP](req, clientIPKey) + + if isIPBlocked(ip, ipFilter) { + logger.Warn("Client IP is blocked by filter: ", ip.String()) + http.Error(w, "Forbidden", http.StatusForbidden) + return + } + + next.ServeHTTP(w, req) + }) +} + +func getIPNets(ipNets []string) ([]string, []string) { + blockedIPNets := []string{} + allowedIPNets := []string{} + + for _, ipNet := range ipNets { + ip, block := strings.CutPrefix(ipNet, "!") + + if block { + blockedIPNets = append(blockedIPNets, ip) + } else { + allowedIPNets = append(allowedIPNets, ip) + } + } + + return allowedIPNets, blockedIPNets +} + +func isIPBlocked(ip net.IP, ipfilter []string) (bool) { + if len(ipfilter) == 0 || ipfilter == nil { + // default: allow all + return false + } + + rawAllowed, rawBlocked := getIPNets(ipfilter) + + allowed := parseIPsAndIPNets(rawAllowed) + blocked := parseIPsAndIPNets(rawBlocked) + + isExplicitlyAllowed := slices.ContainsFunc(allowed, func(try *net.IPNet) bool { + return try.Contains(ip) + }) + isExplicitlyBlocked := slices.ContainsFunc(blocked, func(try *net.IPNet) bool { + return try.Contains(ip) + }) + + // explicit allow > block + if isExplicitlyAllowed { + return false + } + + if isExplicitlyBlocked { + return true + } + + // if any allow rules exist, default is deny + if len(allowed) > 0 { + return true + } + + // only blocked ips -> allow anything not blocked + if len(blocked) > 0 { + return false + } + + // default: allow all + return false +} diff --git a/internals/proxy/middlewares/log.go b/internals/proxy/middlewares/log.go index 96fe536c..116702e2 100644 --- a/internals/proxy/middlewares/log.go +++ b/internals/proxy/middlewares/log.go @@ -1,6 +1,7 @@ package middlewares import ( + "net" "net/http" "strings" @@ -17,6 +18,33 @@ var RequestLogger Middleware = Middleware{ const loggerKey contextKey = "logger" func loggingHandler(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + logger := getLogger(req) + + ip := getContext[net.IP](req, clientIPKey) + + if !logger.IsDev() { + logger.Info(ip.String(), " ", req.Method, " ", req.URL.Path, " ", req.URL.RawQuery) + } else { + body, _ := request.GetReqBody(req) + + if body.Data != nil && !body.Empty { + logger.Dev(ip.String(), " ", req.Method, " ", req.URL.Path, " ", req.URL.RawQuery, body.Data) + } else { + logger.Info(ip.String(), " ", req.Method, " ", req.URL.Path, " ", req.URL.RawQuery) + } + } + + next.ServeHTTP(w, req) + }) +} + +var InternalMiddlewareLogger Middleware = Middleware{ + Name: "_Middleware_Logger", + Use: middlewareLoggerHandler, +} + +func middlewareLoggerHandler(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { conf := getConfigByReq(req) @@ -49,18 +77,6 @@ func loggingHandler(next http.Handler) http.Handler { req = setContext(req, loggerKey, l) - if !l.IsDev() { - l.Info(req.Method, " ", req.URL.Path, " ", req.URL.RawQuery) - } else { - body, _ := request.GetReqBody(req) - - if body.Data != nil && !body.Empty { - l.Dev(req.Method, " ", req.URL.Path, " ", req.URL.RawQuery, body.Data) - } else { - l.Info(req.Method, " ", req.URL.Path, " ", req.URL.RawQuery) - } - } - next.ServeHTTP(w, req) }) } \ No newline at end of file diff --git a/internals/proxy/middlewares/policy.go b/internals/proxy/middlewares/policy.go index bb00ab23..08fbe112 100644 --- a/internals/proxy/middlewares/policy.go +++ b/internals/proxy/middlewares/policy.go @@ -43,7 +43,7 @@ func policyHandler(next http.Handler) http.Handler { shouldBlock, field := doBlock(body.Data, headerData, policies) if shouldBlock { - logger.Warn("User tried to use blocked field: ", field) + logger.Warn("Client tried to use blocked field: ", field) http.Error(w, "Forbidden", http.StatusForbidden) return } diff --git a/internals/proxy/middlewares/port.go b/internals/proxy/middlewares/port.go index 716e04a3..84d30d9f 100644 --- a/internals/proxy/middlewares/port.go +++ b/internals/proxy/middlewares/port.go @@ -34,7 +34,7 @@ func portHandler(next http.Handler) http.Handler { } if port != allowedPort { - logger.Warn("User tried using Token on wrong Port") + logger.Warn("Client tried using Token on wrong Port") onUnauthorized(w) return } diff --git a/internals/proxy/middlewares/proxy.go b/internals/proxy/middlewares/proxy.go new file mode 100644 index 00000000..6762bfe2 --- /dev/null +++ b/internals/proxy/middlewares/proxy.go @@ -0,0 +1,142 @@ +package middlewares + +import ( + "errors" + "net" + "net/http" + "strings" +) + +var InternalProxy Middleware = Middleware{ + Name: "_Proxy", + Use: proxyHandler, +} + +const trustedProxyKey contextKey = "isProxyTrusted" +const clientIPKey contextKey = "clientIP" + +func proxyHandler(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + logger := getLogger(req) + + conf := getConfigByReq(req) + + rawTrustedProxies := conf.SETTINGS.ACCESS.TRUSTED_PROXIES + + if rawTrustedProxies == nil { + rawTrustedProxies = getConfig("").SETTINGS.ACCESS.TRUSTED_PROXIES + } + + var trusted bool + var ip net.IP + + host, _, _ := net.SplitHostPort(req.RemoteAddr) + + ip = net.ParseIP(host) + + if len(rawTrustedProxies) != 0 { + trustedProxies := parseIPsAndIPNets(rawTrustedProxies) + + trusted = isIPInList(ip, trustedProxies) + } + + if trusted { + realIP, err := getRealIP(req) + + if err != nil { + logger.Error("Could not get real IP: ", err.Error()) + } + + if realIP != nil { + ip = realIP + } + } + + req = setContext(req, clientIPKey, ip) + req = setContext(req, trustedProxyKey, trusted) + + next.ServeHTTP(w, req) + }) +} + +func parseIP(str string) (*net.IPNet, error) { + if !strings.Contains(str, "/") { + ip := net.ParseIP(str) + + if ip == nil { + return nil, errors.New("invalid ip: " + str) + } + + var mask net.IPMask + + if ip.To4() != nil { + mask = net.CIDRMask(32, 32) // IPv4 /32 + } else { + mask = net.CIDRMask(128, 128) // IPv6 /128 + } + + return &net.IPNet{IP: ip, Mask: mask}, nil + } + + ip, network, err := net.ParseCIDR(str) + if err != nil { + return nil, err + } + + if !ip.Equal(network.IP) { + var mask net.IPMask + + if ip.To4() != nil { + mask = net.CIDRMask(32, 32) // IPv4 /32 + } else { + mask = net.CIDRMask(128, 128) // IPv6 /128 + } + + return &net.IPNet{IP: ip, Mask: mask}, nil + } + + return network, nil +} + +func parseIPsAndIPNets(array []string) []*net.IPNet { + ipNets := []*net.IPNet{} + + for _, item := range array { + ipNet, err := parseIP(item) + + if err != nil { + continue + } + + ipNets = append(ipNets, ipNet) + } + + return ipNets +} + +func getRealIP(req *http.Request) (net.IP, error) { + XFF := req.Header.Get("X-Forwarded-For") + + if XFF != "" { + ips := strings.Split(XFF, ",") + + realIP := net.ParseIP(strings.TrimSpace(ips[0])) + + if realIP == nil { + return nil, errors.New("malformed x-forwarded-for header") + } + + return realIP, nil + } + + return nil, errors.New("no x-forwarded-for header present") +} + +func isIPInList(ip net.IP, list []*net.IPNet) bool { + for _, net := range list { + if net.Contains(ip) { + return true + } + } + return false +} \ No newline at end of file diff --git a/internals/proxy/middlewares/ratelimit.go b/internals/proxy/middlewares/ratelimit.go index 775b3c00..564f4b4b 100644 --- a/internals/proxy/middlewares/ratelimit.go +++ b/internals/proxy/middlewares/ratelimit.go @@ -35,6 +35,13 @@ func ratelimitHandler(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { logger := getLogger(req) + trusted := getContext[bool](req, trustedClientKey) + + if trusted { + next.ServeHTTP(w, req) + return + } + conf := getConfigByReq(req) rateLimiting := conf.SETTINGS.ACCESS.RATE_LIMITING diff --git a/internals/proxy/proxy.go b/internals/proxy/proxy.go index aae6345f..d532707c 100644 --- a/internals/proxy/proxy.go +++ b/internals/proxy/proxy.go @@ -33,8 +33,12 @@ func (proxy Proxy) Init() http.Handler { handler := m.NewChain(). Use(m.Server). Use(m.Auth). + Use(m.InternalMiddlewareLogger). + Use(m.InternalProxy). + Use(m.InternalClientIP). Use(m.RequestLogger). Use(m.InternalAuthRequirement). + Use(m.IPFilter). Use(m.Port). Use(m.RateLimit). Use(m.Template).