Files
navidrome-meilisearch/plugins/host_network_permissions_base.go
Dongho Kim c251f174ed
Some checks failed
Pipeline: Test, Lint, Build / Get version info (push) Has been cancelled
Pipeline: Test, Lint, Build / Lint Go code (push) Has been cancelled
Pipeline: Test, Lint, Build / Test Go code (push) Has been cancelled
Pipeline: Test, Lint, Build / Test JS code (push) Has been cancelled
Pipeline: Test, Lint, Build / Lint i18n files (push) Has been cancelled
Pipeline: Test, Lint, Build / Check Docker configuration (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (darwin/amd64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (darwin/arm64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/386) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/amd64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm/v5) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm/v6) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm/v7) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (windows/386) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (windows/amd64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Push to GHCR (push) Has been cancelled
Pipeline: Test, Lint, Build / Push to Docker Hub (push) Has been cancelled
Pipeline: Test, Lint, Build / Cleanup digest artifacts (push) Has been cancelled
Pipeline: Test, Lint, Build / Build Windows installers (push) Has been cancelled
Pipeline: Test, Lint, Build / Package/Release (push) Has been cancelled
Pipeline: Test, Lint, Build / Upload Linux PKG (push) Has been cancelled
Close stale issues and PRs / stale (push) Has been cancelled
POEditor import / update-translations (push) Has been cancelled
update
2025-12-08 16:16:23 +01:00

193 lines
4.8 KiB
Go

package plugins
import (
"fmt"
"net"
"net/url"
"regexp"
"strings"
)
// NetworkPermissionsBase contains common functionality for network-based permissions
type networkPermissionsBase struct {
Reason string `json:"reason"`
AllowLocalNetwork bool `json:"allowLocalNetwork,omitempty"`
}
// URLMatcher provides URL pattern matching functionality
type urlMatcher struct{}
// newURLMatcher creates a new URL matcher instance
func newURLMatcher() *urlMatcher {
return &urlMatcher{}
}
// checkURLPolicy performs common checks for a URL against network policies.
func checkURLPolicy(requestURL string, allowLocalNetwork bool) (*url.URL, error) {
parsedURL, err := url.Parse(requestURL)
if err != nil {
return nil, fmt.Errorf("invalid URL: %w", err)
}
// Check local network restrictions
if !allowLocalNetwork {
if err := checkLocalNetwork(parsedURL); err != nil {
return nil, err
}
}
return parsedURL, nil
}
// MatchesURLPattern checks if a URL matches a given pattern
func (m *urlMatcher) MatchesURLPattern(requestURL, pattern string) bool {
// Handle wildcard pattern
if pattern == "*" {
return true
}
// Parse both URLs to handle path matching correctly
reqURL, err := url.Parse(requestURL)
if err != nil {
return false
}
patternURL, err := url.Parse(pattern)
if err != nil {
// If pattern is not a valid URL, treat it as a simple string pattern
regexPattern := m.urlPatternToRegex(pattern)
matched, err := regexp.MatchString(regexPattern, requestURL)
if err != nil {
return false
}
return matched
}
// Match scheme
if patternURL.Scheme != "" && patternURL.Scheme != reqURL.Scheme {
return false
}
// Match host with wildcard support
if !m.matchesHost(reqURL.Host, patternURL.Host) {
return false
}
// Match path with wildcard support
// Special case: if pattern URL has empty path and contains wildcards, allow any path (domain-only wildcard matching)
if (patternURL.Path == "" || patternURL.Path == "/") && strings.Contains(pattern, "*") {
// This is a domain-only wildcard pattern, allow any path
return true
}
if !m.matchesPath(reqURL.Path, patternURL.Path) {
return false
}
return true
}
// urlPatternToRegex converts a URL pattern with wildcards to a regex pattern
func (m *urlMatcher) urlPatternToRegex(pattern string) string {
// Escape special regex characters except *
escaped := regexp.QuoteMeta(pattern)
// Replace escaped \* with regex pattern for wildcard matching
// For subdomain: *.example.com -> [^.]*\.example\.com
// For path: /api/* -> /api/.*
escaped = strings.ReplaceAll(escaped, "\\*", ".*")
// Anchor the pattern to match the full URL
return "^" + escaped + "$"
}
// matchesHost checks if a host matches a pattern with wildcard support
func (m *urlMatcher) matchesHost(host, pattern string) bool {
if pattern == "" {
return true
}
if pattern == "*" {
return true
}
// Handle wildcard patterns anywhere in the host
if strings.Contains(pattern, "*") {
patterns := []string{
strings.ReplaceAll(regexp.QuoteMeta(pattern), "\\*", "[0-9.]+"), // IP pattern
strings.ReplaceAll(regexp.QuoteMeta(pattern), "\\*", "[^.]*"), // Domain pattern
}
for _, regexPattern := range patterns {
fullPattern := "^" + regexPattern + "$"
if matched, err := regexp.MatchString(fullPattern, host); err == nil && matched {
return true
}
}
return false
}
return host == pattern
}
// matchesPath checks if a path matches a pattern with wildcard support
func (m *urlMatcher) matchesPath(path, pattern string) bool {
// Normalize empty paths to "/"
if path == "" {
path = "/"
}
if pattern == "" {
pattern = "/"
}
if pattern == "*" {
return true
}
// Handle wildcard paths
if strings.HasSuffix(pattern, "/*") {
prefix := pattern[:len(pattern)-2] // Remove "/*"
if prefix == "" {
prefix = "/"
}
return strings.HasPrefix(path, prefix)
}
return path == pattern
}
// CheckLocalNetwork checks if the URL is accessing local network resources
func checkLocalNetwork(parsedURL *url.URL) error {
host := parsedURL.Hostname()
// Check for localhost variants
if host == "localhost" || host == "127.0.0.1" || host == "::1" {
return fmt.Errorf("requests to localhost are not allowed")
}
// Try to parse as IP address
ip := net.ParseIP(host)
if ip != nil && isPrivateIP(ip) {
return fmt.Errorf("requests to private IP addresses are not allowed")
}
return nil
}
// IsPrivateIP checks if an IP is loopback, private, or link-local (IPv4/IPv6).
func isPrivateIP(ip net.IP) bool {
if ip == nil {
return false
}
if ip.IsLoopback() || ip.IsPrivate() {
return true
}
// IPv4 link-local: 169.254.0.0/16
if ip4 := ip.To4(); ip4 != nil {
return ip4[0] == 169 && ip4[1] == 254
}
// IPv6 link-local: fe80::/10
if ip16 := ip.To16(); ip16 != nil && ip.To4() == nil {
return ip16[0] == 0xfe && (ip16[1]&0xc0) == 0x80
}
return false
}