Переглянути джерело

feat: dnsname argument type

jamesread 3 тижнів тому
батько
коміт
6eb3827ddd

+ 1 - 0
docs/modules/ROOT/pages/args/types.adoc

@@ -10,6 +10,7 @@ A full list of argument types are below;
 | (default)                   | xref:args/input.adoc[Textbox]           | If a `type:` is not set, and `choices:` is empty, then ascii will be used, and a warning will be logged. It is recommended that you set the type explicitly, rather than relying on defaults.
 | ascii                       | xref:args/input.adoc[Textbox]           | a-z (case insensitive), 0-9, but no spaces or punctuation
 | ascii_identifier            | xref:args/input.adoc[Textbox]           | Like a DNS name, a-Z (case insensitive), 0-9, `-`, `.`, and `_`.
+| dnsname                     | xref:args/input.adoc[Textbox]           | A DNS hostname (RFC 1123). Short names (e.g. `webserver`) and FQDNs (e.g. `webserver.example.com`). Letters/digits/hyphens only, no underscores. Optional trailing dot allowed.
 | shell_safe_identifier       | xref:args/input.adoc[Textbox]           | Like an ascii identifier, but also allows `@` and `+`. Useful for shell-safe usernames and email-style identifiers.
 | ascii_sentence              | xref:args/input.adoc[Textbox]           | a-z (case insensitive), 0-9, with spaces, `.` and `,`.
 | unicode_identifier          | xref:args/input.adoc[Textbox]           | Like an ascii identifier, but allows unicode characters. This is useful for languages that use non-ascii characters, such as Chinese, Japanese, etc.

+ 1 - 1
frontend/resources/vue/views/ArgumentForm.vue

@@ -291,7 +291,7 @@ function getInputType (arg) {
     return 'checkbox'
   }
 
-  if (arg.type === 'ascii_identifier' || arg.type === 'shell_safe_identifier' || arg.type === 'ascii' || arg.type === 'ascii_sentence') {
+  if (arg.type === 'ascii_identifier' || arg.type === 'dnsname' || arg.type === 'shell_safe_identifier' || arg.type === 'ascii' || arg.type === 'ascii_sentence') {
     return 'text'
   }
 

+ 32 - 0
service/internal/executor/arguments.go

@@ -24,6 +24,9 @@ var (
 		"shell_safe_identifier":     `^[a-zA-Z0-9@\.\_\+\-]+$`,
 		"ascii_sentence":            `^[a-zA-Z0-9\-\._, ]+$`,
 	}
+
+	dnsNameLabelPattern      = regexp.MustCompile(`^[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$`)
+	dnsNameAllNumericPattern = regexp.MustCompile(`^[0-9]+$`)
 )
 
 // parseExecArray parses all exec arguments in the action.
@@ -223,6 +226,8 @@ func TypeSafetyCheck(name string, value string, argumentType string) error {
 		return typeSafetyCheckUrl(value)
 	case "datetime":
 		return typeSafetyCheckDatetime(value)
+	case "dnsname":
+		return typeSafetyCheckDnsName(value)
 	}
 
 	return typeSafetyCheckRegex(name, value, argumentType)
@@ -304,6 +309,33 @@ func typeSafetyCheckEmail(value string) error {
 	return nil
 }
 
+// typeSafetyCheckDnsName validates a DNS hostname (RFC 1123 LDH labels).
+// Accepts short names (e.g. webserver) and FQDNs (e.g. webserver.example.com).
+// An optional trailing dot is allowed.
+func typeSafetyCheckDnsName(value string) error {
+	hostname := strings.TrimSuffix(value, ".")
+	if hostname == "" || len(hostname) > 253 {
+		return fmt.Errorf("invalid dnsname length")
+	}
+
+	return typeSafetyCheckDnsNameLabels(strings.Split(hostname, "."))
+}
+
+func typeSafetyCheckDnsNameLabels(labels []string) error {
+	for _, label := range labels {
+		if !dnsNameLabelPattern.MatchString(label) {
+			return fmt.Errorf("invalid dnsname label %q", label)
+		}
+	}
+
+	tld := labels[len(labels)-1]
+	if dnsNameAllNumericPattern.MatchString(tld) {
+		return fmt.Errorf("dnsname top-level label must not be all-numeric")
+	}
+
+	return nil
+}
+
 func typeSafetyCheckDatetime(value string) error {
 	_, err := time.Parse("2006-01-02T15:04:05", value)
 

+ 38 - 0
service/internal/executor/arguments_test.go

@@ -812,6 +812,44 @@ func TestTypeSafetyCheckAsciiIdentifier(t *testing.T) {
 	}
 }
 
+func TestTypeSafetyCheckDnsName(t *testing.T) {
+	tests := []struct {
+		name     string
+		value    string
+		hasError bool
+	}{
+		{"Short name", "webserver", false},
+		{"Localhost", "localhost", false},
+		{"Simple domain", "example.com", false},
+		{"Host with subdomain", "webserver.example.com", false},
+		{"Deep subdomain", "a.b.c.example.co.uk", false},
+		{"Label starting with digit", "1host.example.com", false},
+		{"Trailing dot", "example.com.", false},
+		{"Punycode IDN", "xn--bcher-kva.example", false},
+		{"Underscore", "my_host.example.com", true},
+		{"Space", "example .com", true},
+		{"Leading hyphen label", "-host.example.com", true},
+		{"Trailing hyphen label", "host-.example.com", true},
+		{"Empty label", "example..com", true},
+		{"IP address", "192.168.1.1", true},
+		{"All numeric TLD", "example.123", true},
+		{"All numeric short name", "12345", true},
+		{"Special chars", "exam!ple.com", true},
+		{"Unicode label", "bücher.example.com", true},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			err := TypeSafetyCheck("host", tt.value, "dnsname")
+			if tt.hasError {
+				assert.NotNil(t, err, "Expected error for value '%s'", tt.value)
+			} else {
+				assert.Nil(t, err, "Expected no error for value '%s', but got: %v", tt.value, err)
+			}
+		})
+	}
+}
+
 func TestTypeSafetyCheckShellSafeIdentifier(t *testing.T) {
 	tests := []struct {
 		name     string