Repository: go-playground/validator
Pull request #563 (2020-01-03 19:34:47)

Add hostname_port validator feature
Fixes Or Enhances # 562

- [x] Tests exist or have been written that cover this particular change.

Change Details:
- Add support for `hostname_port` validator. Tests added for this validator.
- Fix regex for `hostnameRegexStringRFC1123` so it will reject hostnames like `extra..com`.

@go-playground/admins


Patch:
diff --git a/baked_in.go b/baked_in.go
index 91fd479..13ec143 100644
--- a/baked_in.go
+++ b/baked_in.go
@@ -166,6 +166,7 @@ var (
 		"html_encoded":         isHTMLEncoded,
 		"url_encoded":          isURLEncoded,
 		"dir":                  isDir,
+		"hostname_port":        isHostnamePort,
 		"lowercase":            isLowercase,
 		"uppercase":            isUppercase,
 	}
@@ -2010,6 +2011,25 @@ func isDir(fl FieldLevel) bool {
 	panic(fmt.Sprintf("Bad field type %T", field.Interface()))
 }
 
+// isHostnamePort validates a <dns>:<port> combination for fields typically used for socket address.
+func isHostnamePort(fl FieldLevel) bool {
+	val := fl.Field().String()
+	host, port, err := net.SplitHostPort(val)
+	if err != nil {
+		return false
+	}
+	// Port must be a iny <= 65535.
+	if portNum, err := strconv.ParseInt(port, 10, 32); err != nil || portNum > 65535 || portNum < 1 {
+		return false
+	}
+
+	// If host is specified, it should match a DNS name
+	if host != "" {
+		return hostnameRegexRFC1123.MatchString(host)
+	}
+	return true
+}
+
 // isLowercase is the validation function for validating if the current field's value is a lowercase string.
 func isLowercase(fl FieldLevel) bool {
 	field := fl.Field()
diff --git a/doc.go b/doc.go
index 0b7a27c..3f304f6 100644
--- a/doc.go
+++ b/doc.go
@@ -1049,6 +1049,13 @@ This is done using os.Stat, which is a platform independent function.
 
 	Usage: dir
 
+HostPort
+
+This validates that a string value contains a valid DNS hostname and port that
+can be used to valiate fields typically passed to sockets and connections.
+
+	Usage: hostname_port
+
 Alias Validators and Tags
 
 NOTE: When returning an error, the tag returned in "FieldError" will be
diff --git a/regexes.go b/regexes.go
index 63fdc1d..b8bf253 100644
--- a/regexes.go
+++ b/regexes.go
@@ -36,11 +36,11 @@ const (
 	latitudeRegexString              = "^[-+]?([1-8]?\\d(\\.\\d+)?|90(\\.0+)?)$"
 	longitudeRegexString             = "^[-+]?(180(\\.0+)?|((1[0-7]\\d)|([1-9]?\\d))(\\.\\d+)?)$"
 	sSNRegexString                   = `^[0-9]{3}[ -]?(0[1-9]|[1-9][0-9])[ -]?([1-9][0-9]{3}|[0-9][1-9][0-9]{2}|[0-9]{2}[1-9][0-9]|[0-9]{3}[1-9])$`
-	hostnameRegexStringRFC952        = `^[a-zA-Z][a-zA-Z0-9\-\.]+[a-zA-Z0-9]$`    // https://tools.ietf.org/html/rfc952
-	hostnameRegexStringRFC1123       = `^[a-zA-Z0-9][a-zA-Z0-9\-\.]+[a-zA-Z0-9]$` // accepts hostname starting with a digit https://tools.ietf.org/html/rfc1123
-	btcAddressRegexString            = `^[13][a-km-zA-HJ-NP-Z1-9]{25,34}$`        // bitcoin address
-	btcAddressUpperRegexStringBech32 = `^BC1[02-9AC-HJ-NP-Z]{7,76}$`              // bitcoin bech32 address https://en.bitcoin.it/wiki/Bech32
-	btcAddressLowerRegexStringBech32 = `^bc1[02-9ac-hj-np-z]{7,76}$`              // bitcoin bech32 address https://en.bitcoin.it/wiki/Bech32
+	hostnameRegexStringRFC952        = `^[a-zA-Z][a-zA-Z0-9\-\.]+[a-zA-Z0-9]$`                                            // https://tools.ietf.org/html/rfc952
+	hostnameRegexStringRFC1123       = `^([a-zA-Z0-9]{1}[a-zA-Z0-9_-]{0,62}){1}(\.[a-zA-Z0-9_]{1}[a-zA-Z0-9_-]{0,62})*?$` // accepts hostname starting with a digit https://tools.ietf.org/html/rfc1123
+	btcAddressRegexString            = `^[13][a-km-zA-HJ-NP-Z1-9]{25,34}$`                                                // bitcoin address
+	btcAddressUpperRegexStringBech32 = `^BC1[02-9AC-HJ-NP-Z]{7,76}$`                                                      // bitcoin bech32 address https://en.bitcoin.it/wiki/Bech32
+	btcAddressLowerRegexStringBech32 = `^bc1[02-9ac-hj-np-z]{7,76}$`                                                      // bitcoin bech32 address https://en.bitcoin.it/wiki/Bech32
 	ethAddressRegexString            = `^0x[0-9a-fA-F]{40}$`
 	ethAddressUpperRegexString       = `^0x[0-9A-F]{40}$`
 	ethAddressLowerRegexString       = `^0x[0-9a-f]{40}$`


Test patch:
diff --git a/validator_test.go b/validator_test.go
index f5d6d01..7c831ed 100644
--- a/validator_test.go
+++ b/validator_test.go
@@ -7954,15 +7954,15 @@ func TestHostnameRFC1123Validation(t *testing.T) {
 
 		if test.expected {
 			if !IsEqual(errs, nil) {
-				t.Fatalf("Index: %d hostname failed Error: %v", i, errs)
+				t.Fatalf("Hostname: %v failed Error: %v", test, errs)
 			}
 		} else {
 			if IsEqual(errs, nil) {
-				t.Fatalf("Index: %d hostname failed Error: %v", i, errs)
+				t.Fatalf("Hostname: %v failed Error: %v", test, errs)
 			} else {
 				val := getError(errs, "", "")
 				if val.Tag() != "hostname_rfc1123" {
-					t.Fatalf("Index: %d hostname failed Error: %v", i, errs)
+					t.Fatalf("Hostname: %v failed Error: %v", i, errs)
 				}
 			}
 		}
@@ -9004,6 +9004,37 @@ func TestGetTag(t *testing.T) {
 	Equal(t, tag, "mytag")
 }
 
+func Test_hostnameport_validator(t *testing.T) {
+
+	type Host struct {
+		Addr string `validate:"hostname_port"`
+	}
+
+	type testInput struct {
+		data     string
+		expected bool
+	}
+	testData := []testInput{
+		{"bad..domain.name:234", false},
+		{"extra.dot.com.", false},
+		{"localhost:1234", true},
+		{"192.168.1.1:1234", true},
+		{":1234", true},
+		{"domain.com:1334", true},
+		{"this.domain.com:234", true},
+		{"domain:75000", false},
+		{"missing.port", false},
+	}
+	for _, td := range testData {
+		h := Host{Addr: td.data}
+		v := New()
+		err := v.Struct(h)
+		if td.expected != (err == nil) {
+			t.Fatalf("Test failed for data: %v Error: %v", td.data, err)
+		}
+	}
+}
+
 func TestLowercaseValidation(t *testing.T) {
 	tests := []struct {
 		param    string