M api/graph/schema.resolvers.go => api/graph/schema.resolvers.go +4 -18
@@ 39,7 39,7 @@ import (
"golang.org/x/image/draw"
"golang.org/x/net/idna"
"netlandish.com/x/gobwebs"
- "netlandish.com/x/gobwebs-oauth2"
+ oauth2 "netlandish.com/x/gobwebs-oauth2"
gaccounts "netlandish.com/x/gobwebs/accounts"
"netlandish.com/x/gobwebs/crypto"
"netlandish.com/x/gobwebs/database"
@@ 1723,10 1723,6 @@ func (r *mutationResolver) AddDomain(ctx context.Context, input model.DomainInpu
lang := links.GetLangFromRequest(server.EchoForContext(ctx).Request(), user)
lt := localizer.GetLocalizer(lang)
- domRegex, err := regexp.Compile(links.DomainPattern)
- if err != nil {
- return nil, fmt.Errorf(lt.Translate("Domain pattern regexp compile error"))
- }
validator := valid.New(ctx)
host, err := idna.Lookup.ToASCII(input.LookupName)
@@ 1747,7 1743,7 @@ func (r *mutationResolver) AddDomain(ctx context.Context, input model.DomainInpu
lt.Translate("Name may not exceed 500 characters")).
WithField("lookupName").
WithCode(valid.ErrValidationCode)
- validator.Expect(domRegex.MatchString(host),
+ validator.Expect(links.IsDomainName(host),
lt.Translate("Invalid FQDN format")).
WithField("lookupName").
WithCode(valid.ErrValidationCode)
@@ 3694,11 3690,6 @@ func (r *mutationResolver) AddAdminDomain(ctx context.Context, input model.Admin
return nil, nil
}
- domRegex, err := regexp.Compile(links.DomainPattern)
- if err != nil {
- return nil, fmt.Errorf(lt.Translate("Domain pattern regexp compile error"))
- }
-
host, err := idna.Lookup.ToASCII(input.LookupName)
validator.Expect(err == nil,
@@ 3728,7 3719,7 @@ func (r *mutationResolver) AddAdminDomain(ctx context.Context, input model.Admin
lt.Translate("Name may not exceed 500 characters")).
WithField("lookupName").
WithCode(valid.ErrValidationCode)
- validator.Expect(domRegex.MatchString(host),
+ validator.Expect(links.IsDomainName(host),
lt.Translate("Invalid FQDN format")).
WithField("lookupName").
WithCode(valid.ErrValidationCode)
@@ 3871,11 3862,6 @@ func (r *mutationResolver) UpdateAdminDomain(ctx context.Context, input model.Up
return nil, nil
}
- domRegex, err := regexp.Compile(links.DomainPattern)
- if err != nil {
- return nil, fmt.Errorf(lt.Translate("Domain pattern regexp compile error"))
- }
-
host, err := idna.Lookup.ToASCII(input.LookupName)
validator.Expect(err == nil,
@@ 3909,7 3895,7 @@ func (r *mutationResolver) UpdateAdminDomain(ctx context.Context, input model.Up
lt.Translate("Name may not exceed 500 characters")).
WithField("lookupName").
WithCode(valid.ErrValidationCode)
- validator.Expect(domRegex.MatchString(input.LookupName),
+ validator.Expect(links.IsDomainName(input.LookupName),
lt.Translate("Invalid FQDN format")).
WithField("lookupName").
WithCode(valid.ErrValidationCode)
M cmd/admin/commands.go => cmd/admin/commands.go +3 -0
@@ 52,6 52,9 @@ func domainValidation(dom, ser string, config *config.Config, domRegex *regexp.R
if err != nil {
return "", fmt.Errorf("Invalid %s domain", ser)
}
+ // Leaving the regexp check because custom deployments may use internal name scheming.
+ // Otherwise we'd use links.IsDomainName. In this case, for admin setup script,
+ // using the regexp is the best choice.
if !domRegex.MatchString(host) {
return "", fmt.Errorf("Invalid %s FQDN format", ser)
M values.go => values.go +68 -1
@@ 31,7 31,7 @@ const (
EmailPattern string = `^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`
// DomainPattern ...
- DomainPattern string = `^(([a-zA-Z]{1})|([a-zA-Z]{1}[a-zA-Z]{1})|([a-zA-Z]{1}[0-9]{1})|([0-9]{1}[a-zA-Z]{1})|([a-zA-Z0-9][a-zA-Z0-9-_]{1,61}[a-zA-Z0-9]))\.([a-zA-Z]{2,6}|[a-zA-Z0-9-]{2,30}\.[a-zA-Z]{2,3})$`
+ DomainPattern string = `^((([a-z0-9][a-z0-9\-]*[a-z0-9])|[a-z0-9])\.?)+$`
)
const (
@@ 102,3 102,70 @@ const (
FilterYearToDay
FilterLast12Months
)
+
+// Taken from https://go.dev/src/net/dnsclient.go to avoid using
+// linkname hack. That is obviously discouraged as noted in the source
+// comments. Also for our case, we want to ensure at least one dot ('.') is
+// included in the given domain name.
+func IsDomainName(s string) bool {
+ // The root domain name is valid. See golang.org/issue/45715.
+ if s == "." {
+ return true
+ }
+
+ // See RFC 1035, RFC 3696.
+ // Presentation format has dots before every label except the first, and the
+ // terminal empty label is optional here because we assume fully-qualified
+ // (absolute) input. We must therefore reserve space for the first and last
+ // labels' length octets in wire format, where they are necessary and the
+ // maximum total length is 255.
+ // So our _effective_ maximum is 253, but 254 is not rejected if the last
+ // character is a dot.
+
+ l := len(s)
+ if l == 0 || l > 254 || l == 254 && s[l-1] != '.' {
+ return false
+ }
+
+ last := byte('.')
+ nonNumeric := false // true once we've seen a letter or hyphen
+ hasDot := false
+ partlen := 0
+ for i := 0; i < len(s); i++ {
+ c := s[i]
+ switch {
+ default:
+ return false
+ case 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || c == '_':
+ nonNumeric = true
+ partlen++
+ case '0' <= c && c <= '9':
+ // fine
+ partlen++
+ case c == '-':
+ // Byte before dash cannot be dot.
+ if last == '.' {
+ return false
+ }
+ partlen++
+ nonNumeric = true
+ case c == '.':
+ // Byte before dot cannot be dot, dash.
+ if last == '.' || last == '-' {
+ return false
+ }
+ if partlen > 63 || partlen == 0 {
+ return false
+ }
+ partlen = 0
+ hasDot = true
+ }
+ last = c
+ }
+
+ if last == '-' || partlen > 63 || !hasDot {
+ return false
+ }
+
+ return nonNumeric
+}