~netlandish/links

93942d6e52324af4da06d6c2c2ceeca3724f8e00 — Peter Sanchez 2 days ago b7d2fdf
Fixed broken fqdn validation when adding a domain.
3 files changed, 75 insertions(+), 19 deletions(-)

M api/graph/schema.resolvers.go
M cmd/admin/commands.go
M values.go
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
}