implement messages and operations for FIND
This commit is contained in:
221
pkg/find/validation.go
Normal file
221
pkg/find/validation.go
Normal file
@@ -0,0 +1,221 @@
|
||||
package find
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidName = errors.New("invalid name format")
|
||||
ErrNameTooLong = errors.New("name exceeds 253 characters")
|
||||
ErrLabelTooLong = errors.New("label exceeds 63 characters")
|
||||
ErrLabelEmpty = errors.New("label is empty")
|
||||
ErrInvalidCharacter = errors.New("invalid character in name")
|
||||
ErrInvalidHyphen = errors.New("label cannot start or end with hyphen")
|
||||
ErrAllNumericLabel = errors.New("label cannot be all numeric")
|
||||
ErrInvalidRecordValue = errors.New("invalid record value")
|
||||
ErrRecordLimitExceeded = errors.New("record limit exceeded")
|
||||
ErrNotOwner = errors.New("not the name owner")
|
||||
ErrNameExpired = errors.New("name registration expired")
|
||||
ErrInRenewalWindow = errors.New("name is in renewal window")
|
||||
ErrNotRenewalWindow = errors.New("not in renewal window")
|
||||
)
|
||||
|
||||
// Name format validation regex
|
||||
var (
|
||||
labelRegex = regexp.MustCompile(`^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$`)
|
||||
allNumeric = regexp.MustCompile(`^[0-9]+$`)
|
||||
)
|
||||
|
||||
// NormalizeName converts a name to lowercase
|
||||
func NormalizeName(name string) string {
|
||||
return strings.ToLower(name)
|
||||
}
|
||||
|
||||
// ValidateName validates a name according to DNS naming rules
|
||||
func ValidateName(name string) error {
|
||||
// Normalize to lowercase
|
||||
name = NormalizeName(name)
|
||||
|
||||
// Check total length
|
||||
if len(name) > 253 {
|
||||
return fmt.Errorf("%w: %d > 253", ErrNameTooLong, len(name))
|
||||
}
|
||||
|
||||
if len(name) == 0 {
|
||||
return fmt.Errorf("%w: name is empty", ErrInvalidName)
|
||||
}
|
||||
|
||||
// Split into labels
|
||||
labels := strings.Split(name, ".")
|
||||
|
||||
for i, label := range labels {
|
||||
if err := validateLabel(label); err != nil {
|
||||
return fmt.Errorf("invalid label %d (%s): %w", i, label, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateLabel validates a single label according to DNS rules
|
||||
func validateLabel(label string) error {
|
||||
// Check length
|
||||
if len(label) == 0 {
|
||||
return ErrLabelEmpty
|
||||
}
|
||||
if len(label) > 63 {
|
||||
return fmt.Errorf("%w: %d > 63", ErrLabelTooLong, len(label))
|
||||
}
|
||||
|
||||
// Check character set and hyphen placement
|
||||
if !labelRegex.MatchString(label) {
|
||||
if strings.HasPrefix(label, "-") || strings.HasSuffix(label, "-") {
|
||||
return ErrInvalidHyphen
|
||||
}
|
||||
return ErrInvalidCharacter
|
||||
}
|
||||
|
||||
// Check not all numeric
|
||||
if allNumeric.MatchString(label) {
|
||||
return ErrAllNumericLabel
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetParentDomain returns the parent domain of a name
|
||||
// e.g., "www.example.com" -> "example.com", "example.com" -> "com", "com" -> ""
|
||||
func GetParentDomain(name string) string {
|
||||
name = NormalizeName(name)
|
||||
parts := strings.Split(name, ".")
|
||||
if len(parts) <= 1 {
|
||||
return "" // TLD has no parent
|
||||
}
|
||||
return strings.Join(parts[1:], ".")
|
||||
}
|
||||
|
||||
// IsTLD returns true if the name is a top-level domain (single label)
|
||||
func IsTLD(name string) bool {
|
||||
name = NormalizeName(name)
|
||||
return !strings.Contains(name, ".")
|
||||
}
|
||||
|
||||
// ValidateIPv4 validates an IPv4 address format
|
||||
func ValidateIPv4(ip string) error {
|
||||
parts := strings.Split(ip, ".")
|
||||
if len(parts) != 4 {
|
||||
return fmt.Errorf("%w: invalid IPv4 format", ErrInvalidRecordValue)
|
||||
}
|
||||
|
||||
for _, part := range parts {
|
||||
var octet int
|
||||
if _, err := fmt.Sscanf(part, "%d", &octet); err != nil {
|
||||
return fmt.Errorf("%w: invalid IPv4 octet: %v", ErrInvalidRecordValue, err)
|
||||
}
|
||||
if octet < 0 || octet > 255 {
|
||||
return fmt.Errorf("%w: IPv4 octet out of range: %d", ErrInvalidRecordValue, octet)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateIPv6 validates an IPv6 address format (simplified check)
|
||||
func ValidateIPv6(ip string) error {
|
||||
// Basic validation - contains colons and valid hex characters
|
||||
if !strings.Contains(ip, ":") {
|
||||
return fmt.Errorf("%w: invalid IPv6 format", ErrInvalidRecordValue)
|
||||
}
|
||||
|
||||
// Split by colons
|
||||
parts := strings.Split(ip, ":")
|
||||
if len(parts) < 3 || len(parts) > 8 {
|
||||
return fmt.Errorf("%w: invalid IPv6 segment count", ErrInvalidRecordValue)
|
||||
}
|
||||
|
||||
// Check for valid hex characters
|
||||
validHex := regexp.MustCompile(`^[0-9a-fA-F]*$`)
|
||||
for _, part := range parts {
|
||||
if part == "" {
|
||||
continue // Allow :: notation
|
||||
}
|
||||
if len(part) > 4 {
|
||||
return fmt.Errorf("%w: IPv6 segment too long", ErrInvalidRecordValue)
|
||||
}
|
||||
if !validHex.MatchString(part) {
|
||||
return fmt.Errorf("%w: invalid IPv6 hex", ErrInvalidRecordValue)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateRecordValue validates a record value based on its type
|
||||
func ValidateRecordValue(recordType, value string) error {
|
||||
switch recordType {
|
||||
case RecordTypeA:
|
||||
return ValidateIPv4(value)
|
||||
case RecordTypeAAAA:
|
||||
return ValidateIPv6(value)
|
||||
case RecordTypeCNAME, RecordTypeMX, RecordTypeNS:
|
||||
return ValidateName(value)
|
||||
case RecordTypeTXT:
|
||||
if len(value) > 1024 {
|
||||
return fmt.Errorf("%w: TXT record exceeds 1024 characters", ErrInvalidRecordValue)
|
||||
}
|
||||
return nil
|
||||
case RecordTypeSRV:
|
||||
return ValidateName(value) // Hostname for SRV
|
||||
default:
|
||||
return fmt.Errorf("%w: unknown record type: %s", ErrInvalidRecordValue, recordType)
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateRecordLimit checks if adding a record would exceed type limits
|
||||
func ValidateRecordLimit(recordType string, currentCount int) error {
|
||||
limit, ok := RecordLimits[recordType]
|
||||
if !ok {
|
||||
return fmt.Errorf("%w: unknown record type: %s", ErrInvalidRecordValue, recordType)
|
||||
}
|
||||
|
||||
if currentCount >= limit {
|
||||
return fmt.Errorf("%w: %s records limited to %d", ErrRecordLimitExceeded, recordType, limit)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidatePriority validates priority value (0-65535)
|
||||
func ValidatePriority(priority int) error {
|
||||
if priority < 0 || priority > 65535 {
|
||||
return fmt.Errorf("%w: priority must be 0-65535", ErrInvalidRecordValue)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateWeight validates weight value (0-65535)
|
||||
func ValidateWeight(weight int) error {
|
||||
if weight < 0 || weight > 65535 {
|
||||
return fmt.Errorf("%w: weight must be 0-65535", ErrInvalidRecordValue)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidatePort validates port value (0-65535)
|
||||
func ValidatePort(port int) error {
|
||||
if port < 0 || port > 65535 {
|
||||
return fmt.Errorf("%w: port must be 0-65535", ErrInvalidRecordValue)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateTrustScore validates trust score (0.0-1.0)
|
||||
func ValidateTrustScore(score float64) error {
|
||||
if score < 0.0 || score > 1.0 {
|
||||
return fmt.Errorf("trust score must be between 0.0 and 1.0, got %f", score)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user