package core import ( "fmt" "net" "regexp" "strings" ) // HostEntry represents a single entry in the hosts file type HostEntry struct { IP string // IP address (IPv4 or IPv6) Hostname string // Primary hostname Aliases []string // Additional hostnames/aliases Comment string // Inline comment Active bool // Whether the entry is enabled (not commented out) Original string // Original line from hosts file for preservation } // NewHostEntry creates a new host entry with validation func NewHostEntry(ip, hostname string) (*HostEntry, error) { entry := &HostEntry{ IP: strings.TrimSpace(ip), Hostname: strings.TrimSpace(hostname), Aliases: make([]string, 0), Active: true, } if err := entry.Validate(); err != nil { return nil, err } return entry, nil } // Validate checks if the host entry is valid func (h *HostEntry) Validate() error { if h.IP == "" { return fmt.Errorf("IP address cannot be empty") } if h.Hostname == "" { return fmt.Errorf("hostname cannot be empty") } // Validate IP address if net.ParseIP(h.IP) == nil { return fmt.Errorf("invalid IP address: %s", h.IP) } // Validate hostname format if err := validateHostname(h.Hostname); err != nil { return fmt.Errorf("invalid hostname: %w", err) } // Validate aliases for _, alias := range h.Aliases { if err := validateHostname(alias); err != nil { return fmt.Errorf("invalid alias '%s': %w", alias, err) } } return nil } // AddAlias adds an alias to the host entry func (h *HostEntry) AddAlias(alias string) error { alias = strings.TrimSpace(alias) if err := validateHostname(alias); err != nil { return fmt.Errorf("invalid alias: %w", err) } // Check for duplicates for _, existing := range h.Aliases { if existing == alias { return fmt.Errorf("alias '%s' already exists", alias) } } h.Aliases = append(h.Aliases, alias) return nil } // AllHostnames returns the primary hostname and all aliases func (h *HostEntry) AllHostnames() []string { result := []string{h.Hostname} result = append(result, h.Aliases...) return result } // String returns the hosts file representation of the entry func (h *HostEntry) String() string { var parts []string // Add IP and hostname parts = append(parts, h.IP, h.Hostname) // Add aliases parts = append(parts, h.Aliases...) line := strings.Join(parts, "\t") // Add comment if present if h.Comment != "" { line += "\t# " + h.Comment } // Add comment prefix if inactive if !h.Active { line = "# " + line } return line } // HostsFile represents the entire hosts file type HostsFile struct { Entries []*HostEntry // All host entries Comments []string // Standalone comment lines Header []string // Header comments at the top of the file } // NewHostsFile creates a new empty hosts file func NewHostsFile() *HostsFile { return &HostsFile{ Entries: make([]*HostEntry, 0), Comments: make([]string, 0), Header: make([]string, 0), } } // AddEntry adds a host entry to the file func (hf *HostsFile) AddEntry(entry *HostEntry) error { if err := entry.Validate(); err != nil { return err } hf.Entries = append(hf.Entries, entry) return nil } // FindEntry finds an entry by hostname func (hf *HostsFile) FindEntry(hostname string) *HostEntry { hostname = strings.TrimSpace(hostname) for _, entry := range hf.Entries { if entry.Hostname == hostname { return entry } for _, alias := range entry.Aliases { if alias == hostname { return entry } } } return nil } // RemoveEntry removes an entry by hostname func (hf *HostsFile) RemoveEntry(hostname string) bool { for i, entry := range hf.Entries { if entry.Hostname == hostname { hf.Entries = append(hf.Entries[:i], hf.Entries[i+1:]...) return true } for _, alias := range entry.Aliases { if alias == hostname { hf.Entries = append(hf.Entries[:i], hf.Entries[i+1:]...) return true } } } return false } // ActiveEntries returns only the active (non-commented) entries func (hf *HostsFile) ActiveEntries() []*HostEntry { var active []*HostEntry for _, entry := range hf.Entries { if entry.Active { active = append(active, entry) } } return active } // validateHostname validates a hostname according to RFC standards func validateHostname(hostname string) error { if len(hostname) == 0 { return fmt.Errorf("hostname cannot be empty") } if len(hostname) > 253 { return fmt.Errorf("hostname too long (max 253 characters)") } // Cannot start or end with hyphen if strings.HasPrefix(hostname, "-") || strings.HasSuffix(hostname, "-") { return fmt.Errorf("hostname cannot start or end with hyphen") } // Split by dots and validate each label labels := strings.Split(hostname, ".") for _, label := range labels { if len(label) == 0 { return fmt.Errorf("invalid hostname format") } // Each label cannot start or end with hyphen if strings.HasPrefix(label, "-") || strings.HasSuffix(label, "-") { return fmt.Errorf("hostname cannot start or end with hyphen") } // Each label must contain only alphanumeric characters and hyphens labelRegex := regexp.MustCompile(`^[a-zA-Z0-9-]+$`) if !labelRegex.MatchString(label) { return fmt.Errorf("invalid hostname format") } } return nil }