279 lines
7.6 KiB
Go
279 lines
7.6 KiB
Go
package validation
|
|
|
|
import (
|
|
"fmt"
|
|
"regexp"
|
|
"strings"
|
|
"unicode"
|
|
)
|
|
|
|
var (
|
|
// Valid pool/dataset name pattern (ZFS naming rules)
|
|
zfsNamePattern = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9_\-\.:]*$`)
|
|
|
|
// Valid username pattern
|
|
usernamePattern = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9_\-\.]{2,31}$`)
|
|
|
|
// Valid share name pattern (SMB naming rules)
|
|
shareNamePattern = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9_\-\.]{0,79}$`)
|
|
|
|
// IQN pattern (simplified - iqn.yyyy-mm.reversed.domain:identifier)
|
|
iqnPattern = regexp.MustCompile(`^iqn\.\d{4}-\d{2}\.[a-zA-Z0-9][a-zA-Z0-9\-\.]*:[a-zA-Z0-9][a-zA-Z0-9\-_\.]*$`)
|
|
|
|
// Email pattern (basic)
|
|
emailPattern = regexp.MustCompile(`^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$`)
|
|
|
|
// CIDR pattern for NFS clients
|
|
cidrPattern = regexp.MustCompile(`^(\d{1,3}\.){3}\d{1,3}(/\d{1,2})?$`)
|
|
)
|
|
|
|
// ValidationError represents a validation error
|
|
type ValidationError struct {
|
|
Field string
|
|
Message string
|
|
}
|
|
|
|
func (e *ValidationError) Error() string {
|
|
if e.Field != "" {
|
|
return fmt.Sprintf("validation error on field '%s': %s", e.Field, e.Message)
|
|
}
|
|
return fmt.Sprintf("validation error: %s", e.Message)
|
|
}
|
|
|
|
// ValidateZFSName validates a ZFS pool or dataset name
|
|
func ValidateZFSName(name string) error {
|
|
if name == "" {
|
|
return &ValidationError{Field: "name", Message: "name cannot be empty"}
|
|
}
|
|
|
|
if len(name) > 256 {
|
|
return &ValidationError{Field: "name", Message: "name too long (max 256 characters)"}
|
|
}
|
|
|
|
if !zfsNamePattern.MatchString(name) {
|
|
return &ValidationError{Field: "name", Message: "invalid characters (allowed: a-z, A-Z, 0-9, _, -, ., :)"}
|
|
}
|
|
|
|
// ZFS names cannot start with certain characters
|
|
if strings.HasPrefix(name, "-") || strings.HasPrefix(name, ".") {
|
|
return &ValidationError{Field: "name", Message: "name cannot start with '-' or '.'"}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ValidateUsername validates a username
|
|
func ValidateUsername(username string) error {
|
|
if username == "" {
|
|
return &ValidationError{Field: "username", Message: "username cannot be empty"}
|
|
}
|
|
|
|
if len(username) < 3 {
|
|
return &ValidationError{Field: "username", Message: "username too short (min 3 characters)"}
|
|
}
|
|
|
|
if len(username) > 32 {
|
|
return &ValidationError{Field: "username", Message: "username too long (max 32 characters)"}
|
|
}
|
|
|
|
if !usernamePattern.MatchString(username) {
|
|
return &ValidationError{Field: "username", Message: "invalid characters (allowed: a-z, A-Z, 0-9, _, -, .)"}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ValidatePassword validates a password
|
|
func ValidatePassword(password string) error {
|
|
if password == "" {
|
|
return &ValidationError{Field: "password", Message: "password cannot be empty"}
|
|
}
|
|
|
|
if len(password) < 8 {
|
|
return &ValidationError{Field: "password", Message: "password too short (min 8 characters)"}
|
|
}
|
|
|
|
if len(password) > 128 {
|
|
return &ValidationError{Field: "password", Message: "password too long (max 128 characters)"}
|
|
}
|
|
|
|
// Check for at least one letter and one number
|
|
hasLetter := false
|
|
hasNumber := false
|
|
|
|
for _, r := range password {
|
|
if unicode.IsLetter(r) {
|
|
hasLetter = true
|
|
}
|
|
if unicode.IsNumber(r) {
|
|
hasNumber = true
|
|
}
|
|
}
|
|
|
|
if !hasLetter {
|
|
return &ValidationError{Field: "password", Message: "password must contain at least one letter"}
|
|
}
|
|
|
|
if !hasNumber {
|
|
return &ValidationError{Field: "password", Message: "password must contain at least one number"}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ValidateEmail validates an email address
|
|
func ValidateEmail(email string) error {
|
|
if email == "" {
|
|
return nil // Email is optional
|
|
}
|
|
|
|
if len(email) > 254 {
|
|
return &ValidationError{Field: "email", Message: "email too long (max 254 characters)"}
|
|
}
|
|
|
|
if !emailPattern.MatchString(email) {
|
|
return &ValidationError{Field: "email", Message: "invalid email format"}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ValidateShareName validates an SMB share name
|
|
func ValidateShareName(name string) error {
|
|
if name == "" {
|
|
return &ValidationError{Field: "name", Message: "share name cannot be empty"}
|
|
}
|
|
|
|
if len(name) > 80 {
|
|
return &ValidationError{Field: "name", Message: "share name too long (max 80 characters)"}
|
|
}
|
|
|
|
if !shareNamePattern.MatchString(name) {
|
|
return &ValidationError{Field: "name", Message: "invalid share name (allowed: a-z, A-Z, 0-9, _, -, .)"}
|
|
}
|
|
|
|
// Reserved names
|
|
reserved := []string{"CON", "PRN", "AUX", "NUL", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9"}
|
|
upperName := strings.ToUpper(name)
|
|
for _, r := range reserved {
|
|
if upperName == r {
|
|
return &ValidationError{Field: "name", Message: fmt.Sprintf("share name '%s' is reserved", name)}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ValidateIQN validates an iSCSI Qualified Name
|
|
func ValidateIQN(iqn string) error {
|
|
if iqn == "" {
|
|
return &ValidationError{Field: "iqn", Message: "IQN cannot be empty"}
|
|
}
|
|
|
|
if len(iqn) > 223 {
|
|
return &ValidationError{Field: "iqn", Message: "IQN too long (max 223 characters)"}
|
|
}
|
|
|
|
if !strings.HasPrefix(iqn, "iqn.") {
|
|
return &ValidationError{Field: "iqn", Message: "IQN must start with 'iqn.'"}
|
|
}
|
|
|
|
// Basic format validation (can be more strict)
|
|
if !iqnPattern.MatchString(iqn) {
|
|
return &ValidationError{Field: "iqn", Message: "invalid IQN format (expected: iqn.yyyy-mm.reversed.domain:identifier)"}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ValidateSize validates a size string (e.g., "10G", "1T")
|
|
func ValidateSize(sizeStr string) error {
|
|
if sizeStr == "" {
|
|
return &ValidationError{Field: "size", Message: "size cannot be empty"}
|
|
}
|
|
|
|
// Pattern: number followed by optional unit (K, M, G, T, P)
|
|
sizePattern := regexp.MustCompile(`^(\d+)([KMGT]?)$`)
|
|
if !sizePattern.MatchString(strings.ToUpper(sizeStr)) {
|
|
return &ValidationError{Field: "size", Message: "invalid size format (expected: number with optional unit K, M, G, T, P)"}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ValidatePath validates a filesystem path
|
|
func ValidatePath(path string) error {
|
|
if path == "" {
|
|
return nil // Path is optional (can be auto-filled)
|
|
}
|
|
|
|
if !strings.HasPrefix(path, "/") {
|
|
return &ValidationError{Field: "path", Message: "path must be absolute (start with /)"}
|
|
}
|
|
|
|
if len(path) > 4096 {
|
|
return &ValidationError{Field: "path", Message: "path too long (max 4096 characters)"}
|
|
}
|
|
|
|
// Check for dangerous path components
|
|
dangerous := []string{"..", "//", "\x00"}
|
|
for _, d := range dangerous {
|
|
if strings.Contains(path, d) {
|
|
return &ValidationError{Field: "path", Message: fmt.Sprintf("path contains invalid component: %s", d)}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ValidateCIDR validates a CIDR notation or hostname
|
|
func ValidateCIDR(cidr string) error {
|
|
if cidr == "" {
|
|
return &ValidationError{Field: "client", Message: "client cannot be empty"}
|
|
}
|
|
|
|
// Allow wildcard
|
|
if cidr == "*" {
|
|
return nil
|
|
}
|
|
|
|
// Check if it's a CIDR
|
|
if cidrPattern.MatchString(cidr) {
|
|
return nil
|
|
}
|
|
|
|
// Check if it's a valid hostname
|
|
hostnamePattern := regexp.MustCompile(`^[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?)*$`)
|
|
if hostnamePattern.MatchString(cidr) {
|
|
return nil
|
|
}
|
|
|
|
return &ValidationError{Field: "client", Message: "invalid client format (expected: CIDR, hostname, or '*')"}
|
|
}
|
|
|
|
// SanitizeString removes potentially dangerous characters
|
|
func SanitizeString(s string) string {
|
|
// Remove null bytes and control characters
|
|
var result strings.Builder
|
|
for _, r := range s {
|
|
if r >= 32 && r != 127 {
|
|
result.WriteRune(r)
|
|
}
|
|
}
|
|
return strings.TrimSpace(result.String())
|
|
}
|
|
|
|
// SanitizePath sanitizes a filesystem path
|
|
func SanitizePath(path string) string {
|
|
// Remove leading/trailing whitespace and normalize slashes
|
|
path = strings.TrimSpace(path)
|
|
path = strings.ReplaceAll(path, "\\", "/")
|
|
|
|
// Remove multiple slashes
|
|
for strings.Contains(path, "//") {
|
|
path = strings.ReplaceAll(path, "//", "/")
|
|
}
|
|
|
|
return path
|
|
}
|