Files
lolcathost/internal/config/validation.go
T
2025-11-28 12:57:23 +00:00

212 lines
4.8 KiB
Go

// Package config provides validation functions for configuration.
package config
import (
"fmt"
"net"
"regexp"
"strings"
)
// domainRegex validates domain names.
var domainRegex = regexp.MustCompile(`^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$|^localhost$`)
// aliasRegex validates alias names.
var aliasRegex = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9_-]{0,62}$`)
// blockedDomains contains domains that cannot be modified.
var blockedDomains = map[string]bool{
"apple.com": true,
"icloud.com": true,
"icloud-content.com": true,
"apple-dns.cn": true,
"apple-dns.net": true,
"mzstatic.com": true,
"itunes.apple.com": true,
"updates.apple.com": true,
}
// ValidationError represents a configuration validation error.
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("%s: %s", e.Field, e.Message)
}
// ValidateConfig validates the entire configuration.
func ValidateConfig(cfg *Config) error {
if cfg == nil {
return &ValidationError{Field: "config", Message: "config is nil"}
}
if err := validateSettings(&cfg.Settings); err != nil {
return err
}
// Track aliases for uniqueness
aliases := make(map[string]bool)
for i, g := range cfg.Groups {
if err := validateGroup(&g, i, aliases); err != nil {
return err
}
}
for i, p := range cfg.Presets {
if err := validatePreset(&p, i, aliases); err != nil {
return err
}
}
return nil
}
func validateSettings(s *Settings) error {
switch s.FlushMethod {
case FlushMethodAuto, FlushMethodDscacheutil, FlushMethodKillall, FlushMethodBoth, "":
// Valid
default:
return &ValidationError{
Field: "settings.flushMethod",
Message: fmt.Sprintf("invalid flush method: %s", s.FlushMethod),
}
}
return nil
}
func validateGroup(g *Group, index int, aliases map[string]bool) error {
if strings.TrimSpace(g.Name) == "" {
return &ValidationError{
Field: fmt.Sprintf("groups[%d].name", index),
Message: "group name is required",
}
}
for i, h := range g.Hosts {
if err := validateHost(&h, index, i, aliases); err != nil {
return err
}
}
return nil
}
func validateHost(h *Host, groupIndex, hostIndex int, aliases map[string]bool) error {
fieldPrefix := fmt.Sprintf("groups[%d].hosts[%d]", groupIndex, hostIndex)
// Validate domain
if !ValidateDomain(h.Domain) {
return &ValidationError{
Field: fieldPrefix + ".domain",
Message: fmt.Sprintf("invalid domain: %s", h.Domain),
}
}
// Check blocked domains
if IsBlockedDomain(h.Domain) {
return &ValidationError{
Field: fieldPrefix + ".domain",
Message: fmt.Sprintf("domain is blocked: %s", h.Domain),
}
}
// Validate IP
if !ValidateIP(h.IP) {
return &ValidationError{
Field: fieldPrefix + ".ip",
Message: fmt.Sprintf("invalid IP address: %s", h.IP),
}
}
// Validate alias
if !ValidateAlias(h.Alias) {
return &ValidationError{
Field: fieldPrefix + ".alias",
Message: fmt.Sprintf("invalid alias: %s", h.Alias),
}
}
// Check alias uniqueness
if aliases[h.Alias] {
return &ValidationError{
Field: fieldPrefix + ".alias",
Message: fmt.Sprintf("duplicate alias: %s", h.Alias),
}
}
aliases[h.Alias] = true
return nil
}
func validatePreset(p *Preset, index int, aliases map[string]bool) error {
fieldPrefix := fmt.Sprintf("presets[%d]", index)
if strings.TrimSpace(p.Name) == "" {
return &ValidationError{
Field: fieldPrefix + ".name",
Message: "preset name is required",
}
}
// Note: We don't validate preset aliases strictly anymore.
// Unknown aliases in presets will simply be skipped when applying the preset.
// This allows presets to survive when hosts are removed from the config.
return nil
}
// ValidateDomain checks if a domain name is valid.
func ValidateDomain(domain string) bool {
if domain == "" {
return false
}
return domainRegex.MatchString(domain)
}
// ValidateIP checks if an IP address is valid (IPv4 or IPv6).
func ValidateIP(ip string) bool {
if ip == "" {
return false
}
return net.ParseIP(ip) != nil
}
// ValidateAlias checks if an alias is valid.
func ValidateAlias(alias string) bool {
if alias == "" {
return false
}
return aliasRegex.MatchString(alias)
}
// IsBlockedDomain checks if a domain is in the blocklist.
func IsBlockedDomain(domain string) bool {
domain = strings.ToLower(domain)
// Check exact match
if blockedDomains[domain] {
return true
}
// Check if it's a subdomain of a blocked domain
for blocked := range blockedDomains {
if strings.HasSuffix(domain, "."+blocked) {
return true
}
}
return false
}
// GetBlockedDomains returns a copy of the blocked domains list.
func GetBlockedDomains() []string {
domains := make([]string, 0, len(blockedDomains))
for d := range blockedDomains {
domains = append(domains, d)
}
return domains
}