diff --git a/pkg/cloudcommon/validators/validators.go b/pkg/cloudcommon/validators/validators.go index 0ecf5a0946..1e68fbd777 100644 --- a/pkg/cloudcommon/validators/validators.go +++ b/pkg/cloudcommon/validators/validators.go @@ -26,6 +26,7 @@ import ( "net" "reflect" "regexp" + "strconv" "strings" "yunion.io/x/jsonutils" @@ -642,6 +643,76 @@ func NewDomainNameValidator(key string) *ValidatorDomainName { return v } +type ValidatorHostPort struct { + ValidatorRegexp + + optionalPort bool + Domain string + Port int + Value string +} + +var regHostPort *regexp.Regexp + +func init() { + // guard against surprise + exp := regutils.DOMAINNAME_REG.String() + if exp != "" && exp[len(exp)-1] == '$' { + exp = exp[:len(exp)-1] + } + exp += "(?::[0-9]{1,5})?" + regHostPort = regexp.MustCompile(exp) +} + +func NewHostPortValidator(key string) *ValidatorHostPort { + v := &ValidatorHostPort{ + ValidatorRegexp: *NewRegexpValidator(key, regHostPort), + } + v.SetParent(v) + return v +} + +func (v *ValidatorHostPort) getValue() interface{} { + return v.Value +} + +func (v *ValidatorHostPort) OptionalPort(optionalPort bool) *ValidatorHostPort { + v.optionalPort = optionalPort + return v +} + +func (v *ValidatorHostPort) Validate(data *jsonutils.JSONDict) error { + err := v.ValidatorRegexp.Validate(data) + if err != nil { + return err + } + hostPort := v.ValidatorRegexp.Value + if hostPort == "" && (v.optional || v.allowEmpty) { + return nil + } + i := strings.IndexRune(hostPort, ':') + if i < 0 { + if v.optionalPort { + v.Value = hostPort + v.Domain = hostPort + return nil + } + return newInvalidValueError(v.Key, "port missing") + } + portStr := hostPort[i+1:] + port, err := strconv.ParseUint(portStr, 10, 16) + if err != nil { + return newInvalidValueError(v.Key, "bad port integer: "+err.Error()) + } + if port <= 0 { + return newInvalidValueError(v.Key, "negative port") + } + v.Value = hostPort + v.Domain = hostPort[:i] + v.Port = int(port) + return nil +} + type ValidatorURLPath struct { ValidatorRegexp } diff --git a/pkg/cloudcommon/validators/validators_test.go b/pkg/cloudcommon/validators/validators_test.go index 9ca18d24d4..af9b66d447 100644 --- a/pkg/cloudcommon/validators/validators_test.go +++ b/pkg/cloudcommon/validators/validators_test.go @@ -52,6 +52,18 @@ func TestURLPathRegexp(t *testing.T) { } } +func TestRegHostPort(t *testing.T) { + inputs := []string{ + "www.yunion.cn", + "www.yunion.cn:9000", + } + for _, in := range inputs { + if !regHostPort.Match([]byte(in)) { + t.Errorf("should match: %q", in) + } + } +} + type C struct { Name string In string @@ -554,6 +566,134 @@ func TestRegexValidator(t *testing.T) { } } +func TestHostPortValidator(t *testing.T) { + type HostPortC struct { + *C + AllowEmpty bool + OptionalPort bool + } + cases := []*HostPortC{ + { + C: &C{ + Name: "missing non-optional", + In: `{}`, + Out: `{}`, + Err: ERR_MISSING_KEY, + ValueWant: "", + }, + }, + { + C: &C{ + Name: "missing optional", + In: `{}`, + Out: `{}`, + Optional: true, + ValueWant: "", + }, + }, + { + C: &C{ + Name: "missing with default", + In: `{}`, + Out: `{s: "example.com"}`, + Default: "example.com", + ValueWant: "example.com", + }, + OptionalPort: true, + }, + { + C: &C{ + Name: "missing with default (has port)", + In: `{}`, + Out: `{s: "example.com:9000"}`, + Default: "example.com:9000", + ValueWant: "example.com:9000", + }, + }, + { + C: &C{ + Name: "valid", + In: `{s: "a.example.com"}`, + Out: `{s: "a.example.com"}`, + ValueWant: "a.example.com", + }, + OptionalPort: true, + }, + { + C: &C{ + Name: "valid (has port)", + In: `{s: "a.example.com:9000"}`, + Out: `{s: "a.example.com:9000"}`, + ValueWant: "a.example.com:9000", + }, + }, + { + C: &C{ + Name: "valid (allow empty)", + In: `{s: ""}`, + Out: `{s: ""}`, + ValueWant: "", + }, + AllowEmpty: true, + }, + { + C: &C{ + Name: "invalid (domain)", + In: `{s: "/.example.com:9000"}`, + Out: `{s: "/.example.com:9000"}`, + ValueWant: "", + Err: ERR_INVALID_VALUE, + }, + }, + { + C: &C{ + Name: "invalid (port)", + In: `{s: "/.example.com:65536"}`, + Out: `{s: "/.example.com:65536"}`, + ValueWant: "", + Err: ERR_INVALID_VALUE, + }, + }, + { + C: &C{ + Name: "invalid (no port)", + In: `{s: "a.example.com"}`, + Out: `{s: "a.example.com"}`, + ValueWant: "", + Err: ERR_INVALID_VALUE, + }, + }, + { + C: &C{ + Name: "invalid (disallow empty)", + In: `{s: ""}`, + Out: `{s: ""}`, + ValueWant: "", + Err: ERR_INVALID_VALUE, + }, + }, + } + for _, c := range cases { + t.Run(c.Name, func(t *testing.T) { + v := NewHostPortValidator("s") + if c.Default != nil { + i := c.Default.(string) + v.Default(i) + } + if c.Optional { + v.Optional(true) + } + if c.OptionalPort { + v.OptionalPort(true) + } + if c.AllowEmpty { + v.AllowEmpty(true) + } + testS(t, v, c.C) + }) + } +} + func TestIPv4Validator(t *testing.T) { var nilIP net.IP localIP := net.IPv4(127, 0, 0, 1).To4()