package validate import ( "bytes" "fmt" "net" "os/exec" "path/filepath" "regexp" "strconv" "strings" "github.com/kballard/go-shellquote" "github.com/pborman/uuid" "github.com/robfig/cron/v3" "github.com/lxc/lxd/shared/osarch" "github.com/lxc/lxd/shared/units" ) // stringInSlice checks whether the supplied string is present in the supplied slice. func stringInSlice(key string, list []string) bool { for _, entry := range list { if entry == key { return true } } return false } // Required returns function that runs one or more validators, all must pass without error. func Required(validators ...func(value string) error) func(value string) error { return func(value string) error { for _, validator := range validators { err := validator(value) if err != nil { return err } } return nil } } // Optional wraps Required() function to make it return nil if value is empty string. func Optional(validators ...func(value string) error) func(value string) error { return func(value string) error { if value == "" { return nil } return Required(validators...)(value) } } // IsInt64 validates whether the string can be converted to an int64. func IsInt64(value string) error { _, err := strconv.ParseInt(value, 10, 64) if err != nil { return fmt.Errorf("Invalid value for an integer %q", value) } return nil } // IsUint8 validates whether the string can be converted to an uint8. func IsUint8(value string) error { _, err := strconv.ParseUint(value, 10, 8) if err != nil { return fmt.Errorf("Invalid value for an integer %q. Must be between 0 and 255", value) } return nil } // IsUint32 validates whether the string can be converted to an uint32. func IsUint32(value string) error { _, err := strconv.ParseUint(value, 10, 32) if err != nil { return fmt.Errorf("Invalid value for uint32 %q: %w", value, err) } return nil } // ParseUint32Range parses a uint32 range in the form "number" or "start-end". // Returns the start number and the size of the range. func ParseUint32Range(value string) (uint32, uint32, error) { rangeParts := strings.SplitN(value, "-", 2) rangeLen := len(rangeParts) if rangeLen != 1 && rangeLen != 2 { return 0, 0, fmt.Errorf("Range must contain a single number or start and end numbers") } startNum, err := strconv.ParseUint(rangeParts[0], 10, 32) if err != nil { return 0, 0, fmt.Errorf("Invalid number %q", value) } var rangeSize uint32 = 1 if rangeLen == 2 { endNum, err := strconv.ParseUint(rangeParts[1], 10, 32) if err != nil { return 0, 0, fmt.Errorf("Invalid end number %q", value) } if startNum >= endNum { return 0, 0, fmt.Errorf("Start number %d must be lower than end number %d", startNum, endNum) } rangeSize += uint32(endNum) - uint32(startNum) } return uint32(startNum), rangeSize, nil } // IsUint32Range validates whether the string is a uint32 range in the form "number" or "start-end". func IsUint32Range(value string) error { _, _, err := ParseUint32Range(value) return err } // IsInRange checks whether an integer is within a specific range. func IsInRange(min int64, max int64) func(value string) error { return func(value string) error { valueInt, err := strconv.ParseInt(value, 10, 64) if err != nil { return fmt.Errorf("Invalid value for an integer %q", value) } if valueInt < min || valueInt > max { return fmt.Errorf("Value isn't within valid range. Must be between %d and %d", min, max) } return nil } } // IsPriority validates priority number. func IsPriority(value string) error { valueInt, err := strconv.ParseInt(value, 10, 64) if err != nil { return fmt.Errorf("Invalid value for an integer %q", value) } if valueInt < 0 || valueInt > 10 { return fmt.Errorf("Invalid value for a limit %q. Must be between 0 and 10", value) } return nil } // IsBool validates if string can be understood as a bool. func IsBool(value string) error { if !stringInSlice(strings.ToLower(value), []string{"true", "false", "yes", "no", "1", "0", "on", "off"}) { return fmt.Errorf("Invalid value for a boolean %q", value) } return nil } // IsOneOf checks whether the string is present in the supplied slice of strings. func IsOneOf(valid ...string) func(value string) error { return func(value string) error { if !stringInSlice(value, valid) { return fmt.Errorf("Invalid value %q (not one of %s)", value, valid) } return nil } } // IsAny accepts all strings as valid. func IsAny(value string) error { return nil } // IsListOf returns a validator for a comma separated list of values. func IsListOf(validator func(value string) error) func(value string) error { return func(value string) error { for _, v := range strings.Split(value, ",") { v = strings.TrimSpace(v) err := validator(v) if err != nil { return fmt.Errorf("Item %q: %w", v, err) } } return nil } } // IsNotEmpty requires a non-empty string. func IsNotEmpty(value string) error { if value == "" { return fmt.Errorf("Required value") } return nil } // IsSize checks if string is valid size according to units.ParseByteSizeString. func IsSize(value string) error { _, err := units.ParseByteSizeString(value) if err != nil { return err } return nil } // IsDeviceID validates string is four lowercase hex characters suitable as Vendor or Device ID. func IsDeviceID(value string) error { regexHexLc, err := regexp.Compile("^[0-9a-f]+$") if err != nil { return err } if len(value) != 4 || !regexHexLc.MatchString(value) { return fmt.Errorf("Invalid value, must be four lower case hex characters") } return nil } // IsInterfaceName validates a real network interface name. func IsInterfaceName(value string) error { // Validate the length. if len(value) < 2 { return fmt.Errorf("Network interface is too short (minimum 2 characters)") } if len(value) > 15 { return fmt.Errorf("Network interface is too long (maximum 15 characters)") } // Validate the character set. match, _ := regexp.MatchString("^[-_a-zA-Z0-9.]+$", value) if !match { return fmt.Errorf("Network interface contains invalid characters") } return nil } // IsNetworkMAC validates an Ethernet MAC address. e.g. "00:00:5e:00:53:01". func IsNetworkMAC(value string) error { _, err := net.ParseMAC(value) // Check is valid Ethernet MAC length and delimiter. if err != nil || len(value) != 17 || strings.ContainsAny(value, "-.") { return fmt.Errorf("Invalid MAC address, must be 6 bytes of hex separated by colons") } return nil } // IsNetworkAddress validates an IP (v4 or v6) address string. func IsNetworkAddress(value string) error { ip := net.ParseIP(value) if ip == nil { return fmt.Errorf("Not an IP address %q", value) } return nil } // IsNetworkAddressList validates a comma delimited list of IPv4 or IPv6 addresses. func IsNetworkAddressList(value string) error { for _, v := range strings.Split(value, ",") { v = strings.TrimSpace(v) err := IsNetworkAddress(v) if err != nil { return err } } return nil } // IsNetwork validates an IP network CIDR string. func IsNetwork(value string) error { ip, subnet, err := net.ParseCIDR(value) if err != nil { return err } if ip.String() != subnet.IP.String() { return fmt.Errorf("Not an IP network address %q", value) } return nil } // IsNetworkList validates a comma delimited list of IP network CIDR strings. func IsNetworkList(value string) error { for _, network := range strings.Split(value, ",") { err := IsNetwork(strings.TrimSpace(network)) if err != nil { return err } } return nil } // IsNetworkAddressCIDR validates an IP addresss string in CIDR format. func IsNetworkAddressCIDR(value string) error { _, _, err := net.ParseCIDR(value) if err != nil { return err } return nil } // IsNetworkRange validates an IP range in the format "start-end". func IsNetworkRange(value string) error { ips := strings.SplitN(value, "-", 2) if len(ips) != 2 { return fmt.Errorf("IP range must contain start and end IP addresses") } startIP := net.ParseIP(ips[0]) if startIP == nil { return fmt.Errorf("Start not an IP address %q", ips[0]) } endIP := net.ParseIP(ips[1]) if endIP == nil { return fmt.Errorf("End not an IP address %q", ips[1]) } if (startIP.To4() != nil) != (endIP.To4() != nil) { return fmt.Errorf("Start and end IP addresses are not in same family") } if bytes.Compare(startIP, endIP) > 0 { return fmt.Errorf("Start IP address must be before or equal to end IP address") } return nil } // IsNetworkV4 validates an IPv4 CIDR string. func IsNetworkV4(value string) error { ip, subnet, err := net.ParseCIDR(value) if err != nil { return err } if ip.To4() == nil { return fmt.Errorf("Not an IPv4 network %q", value) } if ip.String() != subnet.IP.String() { return fmt.Errorf("Not an IPv4 network address %q", value) } return nil } // IsNetworkV4List validates a comma delimited list of IPv4 CIDR strings. func IsNetworkV4List(value string) error { for _, network := range strings.Split(value, ",") { network = strings.TrimSpace(network) err := IsNetworkV4(network) if err != nil { return err } } return nil } // IsNetworkAddressV4 validates an IPv4 addresss string. func IsNetworkAddressV4(value string) error { ip := net.ParseIP(value) if ip == nil || ip.To4() == nil { return fmt.Errorf("Not an IPv4 address %q", value) } return nil } // IsNetworkAddressV4List validates a comma delimited list of IPv4 addresses. func IsNetworkAddressV4List(value string) error { for _, v := range strings.Split(value, ",") { v = strings.TrimSpace(v) err := IsNetworkAddressV4(v) if err != nil { return err } } return nil } // IsNetworkAddressCIDRV4 validates an IPv4 addresss string in CIDR format. func IsNetworkAddressCIDRV4(value string) error { ip, subnet, err := net.ParseCIDR(value) if err != nil { return err } if ip.To4() == nil { return fmt.Errorf("Not an IPv4 address %q", value) } if ip.String() == subnet.IP.String() { return fmt.Errorf("Not a usable IPv4 address %q", value) } return nil } // IsNetworkRangeV4 validates an IPv4 range in the format "start-end". func IsNetworkRangeV4(value string) error { ips := strings.SplitN(value, "-", 2) if len(ips) != 2 { return fmt.Errorf("IP range must contain start and end IP addresses") } for _, ip := range ips { err := IsNetworkAddressV4(ip) if err != nil { return err } } return nil } // IsNetworkRangeV4List validates a comma delimited list of IPv4 ranges. func IsNetworkRangeV4List(value string) error { for _, ipRange := range strings.Split(value, ",") { err := IsNetworkRangeV4(strings.TrimSpace(ipRange)) if err != nil { return err } } return nil } // IsNetworkV6 validates an IPv6 CIDR string. func IsNetworkV6(value string) error { ip, subnet, err := net.ParseCIDR(value) if err != nil { return err } if ip == nil || ip.To4() != nil { return fmt.Errorf("Not an IPv6 network %q", value) } if ip.String() != subnet.IP.String() { return fmt.Errorf("Not an IPv6 network address %q", value) } return nil } // IsNetworkV6List validates a comma delimited list of IPv6 CIDR strings. func IsNetworkV6List(value string) error { for _, network := range strings.Split(value, ",") { network = strings.TrimSpace(network) err := IsNetworkV6(network) if err != nil { return err } } return nil } // IsNetworkAddressV6 validates an IPv6 addresss string. func IsNetworkAddressV6(value string) error { ip := net.ParseIP(value) if ip == nil || ip.To4() != nil { return fmt.Errorf("Not an IPv6 address %q", value) } return nil } // IsNetworkAddressV6List validates a comma delimited list of IPv6 addresses. func IsNetworkAddressV6List(value string) error { for _, v := range strings.Split(value, ",") { v = strings.TrimSpace(v) err := IsNetworkAddressV6(v) if err != nil { return err } } return nil } // IsNetworkAddressCIDRV6 validates an IPv6 addresss string in CIDR format. func IsNetworkAddressCIDRV6(value string) error { ip, subnet, err := net.ParseCIDR(value) if err != nil { return err } if ip.To4() != nil { return fmt.Errorf("Not an IPv6 address %q", value) } if ip.String() == subnet.IP.String() { return fmt.Errorf("Not a usable IPv6 address %q", value) } return nil } // IsNetworkRangeV6 validates an IPv6 range in the format "start-end". func IsNetworkRangeV6(value string) error { ips := strings.SplitN(value, "-", 2) if len(ips) != 2 { return fmt.Errorf("IP range must contain start and end IP addresses") } for _, ip := range ips { err := IsNetworkAddressV6(ip) if err != nil { return err } } return nil } // IsNetworkRangeV6List validates a comma delimited list of IPv6 ranges. func IsNetworkRangeV6List(value string) error { for _, ipRange := range strings.Split(value, ",") { err := IsNetworkRangeV6(strings.TrimSpace(ipRange)) if err != nil { return err } } return nil } // IsNetworkVLAN validates a VLAN ID. func IsNetworkVLAN(value string) error { vlanID, err := strconv.Atoi(value) if err != nil { return fmt.Errorf("Invalid VLAN ID %q", value) } if vlanID < 0 || vlanID > 4094 { return fmt.Errorf("Out of VLAN ID range (0-4094) %q", value) } return nil } // IsNetworkMTU validates MTU number >= 1280 and <= 16384. // Anything below 68 and the kernel doesn't allow IPv4, anything below 1280 and the kernel doesn't allow IPv6. // So require an IPv6-compatible MTU as the low value and cap at the max ethernet jumbo frame size. func IsNetworkMTU(value string) error { mtu, err := strconv.ParseUint(value, 10, 32) if err != nil { return fmt.Errorf("Invalid MTU %q", value) } if mtu < 1280 || mtu > 16384 { return fmt.Errorf("Out of MTU range (1280-16384) %q", value) } return nil } // IsNetworkPort validates an IP port number >= 0 and <= 65535. func IsNetworkPort(value string) error { port, err := strconv.ParseUint(value, 10, 32) if err != nil { return fmt.Errorf("Invalid port number %q", value) } if port < 0 || port > 65535 { return fmt.Errorf("Out of port number range (0-65535) %q", value) } return nil } // IsNetworkPortRange validates an IP port range in the format "port" or "start-end". func IsNetworkPortRange(value string) error { ports := strings.SplitN(value, "-", 2) portsLen := len(ports) if portsLen != 1 && portsLen != 2 { return fmt.Errorf("Port range must contain either a single port or start and end port numbers") } startPort, err := strconv.ParseUint(ports[0], 10, 32) if err != nil { return fmt.Errorf("Invalid port number %q", value) } if portsLen == 2 { endPort, err := strconv.ParseUint(ports[1], 10, 32) if err != nil { return fmt.Errorf("Invalid end port number %q", value) } if startPort >= endPort { return fmt.Errorf("Start port %d must be lower than end port %d", startPort, endPort) } } return nil } // IsURLSegmentSafe validates whether value can be used in a URL segment. func IsURLSegmentSafe(value string) error { for _, char := range []string{"/", "?", "&", "+"} { if strings.Contains(value, char) { return fmt.Errorf("Cannot contain %q", char) } } return nil } // IsUUID validates whether a value is a UUID. func IsUUID(value string) error { if uuid.Parse(value) == nil { return fmt.Errorf("Invalid UUID") } return nil } // IsPCIAddress validates whether a value is a PCI address. func IsPCIAddress(value string) error { regexHex, err := regexp.Compile(`^([0-9a-fA-F]{4}?:)?[0-9a-fA-F]{2}:[0-9a-fA-F]{2}\.[0-9a-fA-F]$`) if err != nil { return err } if !regexHex.MatchString(value) { return fmt.Errorf("Invalid PCI address") } return nil } // IsCompressionAlgorithm validates whether a value is a valid compression algorithm and is available on the system. func IsCompressionAlgorithm(value string) error { if value == "none" { return nil } // Going to look up tar2sqfs executable binary if value == "squashfs" { value = "tar2sqfs" } // Parse the command. fields, err := shellquote.Split(value) if err != nil { return err } _, err = exec.LookPath(fields[0]) return err } // IsArchitecture validates whether the value is a valid LXD architecture name. func IsArchitecture(value string) error { return IsOneOf(osarch.SupportedArchitectures()...)(value) } // IsCron checks that it's a valid cron pattern or alias. func IsCron(aliases []string) func(value string) error { return func(value string) error { isValid := func(value string) error { // Accept valid aliases. for _, alias := range aliases { if alias == value { return nil } } if len(strings.Split(value, " ")) != 5 { return fmt.Errorf("Schedule must be of the form: ") } _, err := cron.ParseStandard(value) if err != nil { return fmt.Errorf("Error parsing schedule: %w", err) } return nil } // Can be comma+space separated (just commas are valid cron pattern). value = strings.ToLower(value) triggers := strings.Split(value, ", ") for _, trigger := range triggers { err := isValid(trigger) if err != nil { return err } } return nil } } // IsListenAddress returns a validator for a listen address. func IsListenAddress(allowDNS bool, allowWildcard bool, requirePort bool) func(value string) error { return func(value string) error { // Validate address format and port. host, _, err := net.SplitHostPort(value) if err != nil { if requirePort { return fmt.Errorf("A port is required as part of the address") } host = value } // Validate wildcard. if stringInSlice(host, []string{"", "::", "[::]", "0.0.0.0"}) { if !allowWildcard { return fmt.Errorf("Wildcard addresses aren't allowed") } return nil } // Validate DNS. ip := net.ParseIP(strings.Trim(host, "[]")) if ip != nil { return nil } if !allowDNS { return fmt.Errorf("DNS names not allowed in address") } _, err = net.LookupHost(host) if err != nil { return fmt.Errorf("Couldn't resolve %q", host) } return nil } } // IsAbsFilePath checks if value is an absolute file path. func IsAbsFilePath(value string) error { if !filepath.IsAbs(value) { return fmt.Errorf("Must be absolute file path") } return nil } // ParseNetworkVLANRange parses a VLAN range in the form "number" or "start-end". // Returns the start number and the number of items in the range. func ParseNetworkVLANRange(vlan string) (int, int, error) { err := IsNetworkVLAN(vlan) if err == nil { vlanRangeStart, err := strconv.Atoi(vlan) if err != nil { return -1, -1, err } return vlanRangeStart, 1, nil } vlanRange := strings.Split(vlan, "-") if len(vlanRange) != 2 { return -1, -1, fmt.Errorf("Invalid VLAN range input: %s", vlan) } if IsNetworkVLAN(vlanRange[0]) != nil || IsNetworkVLAN(vlanRange[1]) != nil { return -1, -1, fmt.Errorf("Invalid VLAN range boundary. start:%s, end:%s", vlanRange[0], vlanRange[1]) } vlanRangeStart, err := strconv.Atoi(vlanRange[0]) if err != nil { return -1, -1, err } vlanRangeEnd, err := strconv.Atoi(vlanRange[1]) if err != nil { return -1, -1, err } if vlanRangeStart > vlanRangeEnd { return -1, -1, fmt.Errorf("Invalid VLAN range boundary. start:%d is higher than end:%d", vlanRangeStart, vlanRangeEnd) } return vlanRangeStart, vlanRangeEnd - vlanRangeStart + 1, nil } // IsHostname checks the string is valid DNS hostname. func IsHostname(name string) error { // Validate length if len(name) < 1 || len(name) > 63 { return fmt.Errorf("Name must be 1-63 characters long") } // Validate first character if strings.HasPrefix(name, "-") { return fmt.Errorf(`Name must not start with "-" character`) } // Validate last character if strings.HasSuffix(name, "-") { return fmt.Errorf(`Name must not end with "-" character`) } _, err := strconv.Atoi(string(name[0])) if err == nil { return fmt.Errorf("Name must not start with a number") } match, err := regexp.MatchString(`^[\-a-zA-Z0-9]+$`, name) if err != nil { return err } if !match { return fmt.Errorf("Name can only contain alphanumeric and hyphen characters") } return nil } // IsDeviceName checks name is 1-63 characters long, doesn't start with a full stop and contains only alphanumeric, // forward slash, hyphen, colon, underscore and full stop characters. func IsDeviceName(name string) error { if len(name) < 1 || len(name) > 63 { return fmt.Errorf("Name must be 1-63 characters long") } if string(name[0]) == "." { return fmt.Errorf(`Name must not start with "." character`) } match, err := regexp.MatchString(`^[\/\.\-:_a-zA-Z0-9]+$`, name) if err != nil { return err } if !match { return fmt.Errorf("Name can only contain alphanumeric, forward slash, hyphen, colon, underscore and full stop characters") } return nil }