From 3f79dbbb08f6abbcc88c42f602393bc6eb0c3c66 Mon Sep 17 00:00:00 2001 From: Fufu Date: Tue, 20 Aug 2024 11:23:29 +0800 Subject: [PATCH] feat: add GetDomain() and CheckDomain() --- domain.go | 78 +++++++++++++++++++++++++++++++++++++++++ domain_test.go | 94 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 172 insertions(+) create mode 100644 domain.go create mode 100644 domain_test.go diff --git a/domain.go b/domain.go new file mode 100644 index 0000000..f5fa55b --- /dev/null +++ b/domain.go @@ -0,0 +1,78 @@ +package utils + +import ( + "errors" + "fmt" + "strings" + "unicode/utf8" +) + +// GetDomain 检查并返回清除前后空白的域名 +func GetDomain(name string) string { + name = strings.TrimSpace(name) + if CheckDomain(name) != nil { + return "" + } + return name +} + +// CheckDomain returns an error if the host name is not valid. +// See https://tools.ietf.org/html/rfc1034#section-3.5 and +// https://tools.ietf.org/html/rfc1123#section-2. +// Ref: chmike/domain +func CheckDomain(name string) error { + if len(name) == 0 { + return errors.New("domain name is empty") + } + if name[len(name)-1] == '.' { + if len(name) > 254 { + return fmt.Errorf("domain name length is %d, can't exceed 254 with a trailing dot", len(name)) + } + name = name[:len(name)-1] // drop valid trailing dot + if len(name) == 0 { + return errors.New("domain name is a single dot") + } + } else if len(name) > 253 { + return fmt.Errorf("domain name length is %d, can't exceed 253 without a trailing dot", len(name)) + } + var l int + for i := 0; i < len(name); i++ { + b := name[i] + if b == '.' { + // check domain labels validity + switch { + case i == l: + return fmt.Errorf("domain has an empty label at offset %d", l) + case i-l > 63: + return fmt.Errorf("domain byte length of label '%s' is %d, can't exceed 63", name[l:i], i-l) + case name[l] == '-': + return fmt.Errorf("domain label '%s' at offset %d begins with a hyphen", name[l:i], l) + case name[i-1] == '-': + return fmt.Errorf("domain label '%s' at offset %d ends with a hyphen", name[l:i], l) + } + l = i + 1 + continue + } + // test label character validity, note: tests are ordered by decreasing validity frequency + if !(b >= 'a' && b <= 'z' || b >= '0' && b <= '9' || b == '-' || b >= 'A' && b <= 'Z') { + // show the printable unicode character starting at byte offset i + c, _ := utf8.DecodeRuneInString(name[i:]) + if c == utf8.RuneError { + return fmt.Errorf("domain has invalid rune at offset %d", i) + } + return fmt.Errorf("domain has invalid character '%c' at offset %d", c, i) + } + } + // check top level domain validity + switch { + case len(name)-l > 63: + return fmt.Errorf("domain's top level domain '%s' has byte length %d, can't exceed 63", name[l:], len(name)-l) + case name[l] == '-': + return fmt.Errorf("domain's top level domain '%s' at offset %d begin with a hyphen", name[l:], l) + case name[len(name)-1] == '-': + return fmt.Errorf("domain's top level domain '%s' at offset %d ends with a hyphen", name[l:], l) + case name[l] >= '0' && name[l] <= '9': + return fmt.Errorf("domain's top level domain '%s' at offset %d begins with a digit", name[l:], l) + } + return nil +} diff --git a/domain_test.go b/domain_test.go new file mode 100644 index 0000000..50bf054 --- /dev/null +++ b/domain_test.go @@ -0,0 +1,94 @@ +package utils + +import ( + "strings" + "testing" + + "github.com/fufuok/utils/assert" +) + +var longName = strings.Repeat(strings.Repeat("a", 63)+".", 4) + +func TestDomainRegexp(t *testing.T) { + f63 := strings.Repeat("f", 63) + cases := []struct { + in, want string + }{ + {"f", "f"}, + {"f.cn", "f.cn"}, + {"7.cn", "7.cn"}, + {"f-7.com.cn", "f-7.com.cn"}, + {"f--f.cn", "f--f.cn"}, + {"-f.cn", ""}, + {"f-.cn", ""}, + {"f_.cn", ""}, + {"f_f.cn", ""}, + {"f.f", "f.f"}, + {"f.77", ""}, + {"f." + f63, "f." + f63}, + {"f." + f63 + ".cn", "f." + f63 + ".cn"}, + {"f.f" + f63, ""}, + {"f.f" + f63 + ".cn", ""}, + {"_.f.cn", ""}, + {"*.f.cn", ""}, + {"xn--fiq06l2rdsvs.xn--vuq861b.xn--fiqs8s", "xn--fiq06l2rdsvs.xn--vuq861b.xn--fiqs8s"}, + } + for _, c := range cases { + assert.Equal(t, c.want, GetDomain(c.in), c.in) + } +} + +// Ref: chmike/domain +func TestCheckDomain(t *testing.T) { + tests := []struct { + n string + e string + }{ + // 0 + {n: "", e: "domain name is empty"}, + {n: "example.com"}, + {n: "EXAMPLE.com"}, + {n: "foo-bar.com"}, + {n: "www1.foo-bar.com"}, + // 5 + {n: "192.168.1.1.example.com"}, + {n: strings.Repeat("a", 70) + ".com", e: "domain byte length of label 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' is 70, can't exceed 63"}, + {n: "example.com" + strings.Repeat("a", 70), e: "domain's top level domain 'comaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' has byte length 73, can't exceed 63"}, + {n: "?", e: "domain has invalid character '?' at offset 0"}, + {n: "\t", e: "domain has invalid character '\t' at offset 0"}, + // 10 + {n: "exàmple.com", e: "domain has invalid character 'à' at offset 2"}, + {n: "www.\xbd\xb2.com", e: "domain has invalid rune at offset 4"}, + {n: "-example.com", e: "domain label '-example' at offset 0 begins with a hyphen"}, + {n: "example-.com", e: "domain label 'example-' at offset 0 ends with a hyphen"}, + {n: "example.-com", e: "domain's top level domain '-com' at offset 8 begin with a hyphen"}, + // 15 + {n: "example.com-", e: "domain's top level domain 'com-' at offset 8 ends with a hyphen"}, + {n: "example.1com", e: "domain's top level domain '1com' at offset 8 begins with a digit"}, + {n: ".example.com", e: "domain has an empty label at offset 0"}, + {n: "example..com", e: "domain has an empty label at offset 8"}, + {n: "example.com."}, + // 20 + {n: longName, e: "domain name length is 256, can't exceed 254 with a trailing dot"}, + {n: longName[:253]}, + {n: longName[:253] + "."}, + {n: longName[:254], e: "domain name length is 254, can't exceed 253 without a trailing dot"}, + {n: longName[:255] + ".", e: "domain name length is 256, can't exceed 254 with a trailing dot"}, + // 25 + {n: ".", e: "domain name is a single dot"}, + } + for i, test := range tests { + err := CheckDomain(test.n) + if (err == nil) != (test.e == "") { + if err != nil { + t.Errorf("%2d unexpected error: %q", i, err) + } else { + t.Errorf("%2d unexpected nil error", i) + } + continue + } + if err != nil && err.Error() != test.e { + t.Errorf("%2d expect error %q, got %q", i, test.e, err) + } + } +}