mirror of
https://github.com/shokinn/hosts-go.git
synced 2025-08-23 08:33:02 +00:00
- Created activeContext.md and productContext.md to outline project goals and current focus. - Established progress.md to track project milestones and tasks. - Developed projectbrief.md detailing application overview, requirements, and directory structure. - Documented systemPatterns.md to describe architecture and design patterns used. - Compiled techContext.md to specify technologies and development setup. - Implemented comprehensive unit tests in models_test.go for HostEntry and HostsFile functionalities.
221 lines
5.2 KiB
Go
221 lines
5.2 KiB
Go
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
|
|
}
|