Skip to content

Instantly share code, notes, and snippets.

@chmike
Last active August 20, 2024 02:29
Show Gist options
  • Save chmike/d4126a3247a6d9a70922fc0e8b4f4013 to your computer and use it in GitHub Desktop.
Save chmike/d4126a3247a6d9a70922fc0e8b4f4013 to your computer and use it in GitHub Desktop.
Check domain name validity in Go
// Please use the package https://github.com/chmike/domain as is it maintained up to date with tests.
// checkDomain returns an error if the domain name is not valid.
// See https://tools.ietf.org/html/rfc1034#section-3.5 and
// https://tools.ietf.org/html/rfc1123#section-2.
func checkDomain(name string) error {
switch {
case len(name) == 0:
return nil // an empty domain name will result in a cookie without a domain restriction
case len(name) > 255:
return fmt.Errorf("domain name length is %d, can't exceed 255", 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 invalid character '.' at offset %d, label can't begin with a period", i)
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 l == len(name):
return fmt.Errorf("domain has missing top level domain, domain can't end with a period")
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
}
@jftuga
Copy link

jftuga commented Oct 7, 2023

func main() {
	d := "example.com"
	err := checkDomain(d)
	if err != nil {
		fmt.Println(err)
	} else {
		fmt.Println("OK:", d)
	}

	d = "example,com"
	err = checkDomain(d)
	if err != nil {
		fmt.Println(err)
	} else {
		fmt.Println("OK:", d)
	}

	d = "exam\\ple.com"
	err = checkDomain(d)
	if err != nil {
		fmt.Println(err)
	} else {
		fmt.Println("OK:", d)
	}

	d = "ex\ample.com"
	err = checkDomain(d)
	if err != nil {
		fmt.Println(err)
	} else {
		fmt.Println("OK:", d)
	}
}

OK: example.com
cookie domain: invalid character ',' at offset 7
cookie domain: invalid character '\' at offset 4
cookie domain: invalid character '�' at offset 2

Go Playground: https://go.dev/play/p/ZukeaABDeDa

@chmike
Copy link
Author

chmike commented Oct 7, 2023

Nice finding. Thank you. The character sequence "\a" is an escape sequence for the bell control character. The error string shouldn't contain a control character. I'll fix that ASAP. Any suggestion is welcome as it would save me time.

@jftuga
Copy link

jftuga commented Oct 11, 2023

Maybe use hex value in error message, such as 0x07 for the bell control character?

@chmike
Copy link
Author

chmike commented Oct 15, 2023

The reference implementation is in my SecureCookies package where this problem has already been fixed for some time now. Many tests are also provided. I should probably create a dedicated package instead of a gist. What do you think ?

Here is the fixed code

// checkDomain returns an error if the domain name is not valid.
// See https://tools.ietf.org/html/rfc1034#section-3.5 and
// https://tools.ietf.org/html/rfc1123#section-2.
func checkDomain(name string) error {
	switch {
	case len(name) == 0:
		return nil // an empty domain name will result in a cookie without a domain restriction
	case len(name) > 255:
		return fmt.Errorf("domain name length is %d, can't exceed 255", 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 invalid character '.' at offset %d, label can't begin with a period", i)
			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 l == len(name):
		return fmt.Errorf("domain has missing top level domain, domain can't end with a period")
	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
}

@jftuga
Copy link

jftuga commented Oct 21, 2023

If you made a dedicated package, I could see others importing and using it as this is very useful.

@chmike
Copy link
Author

chmike commented Oct 28, 2023

Sorry for the delay. It already exists here : https://github.com/chmike/domain.
I'll update the gist and add a redirection to the package. Thanks for the suggestion.

@jftuga
Copy link

jftuga commented Oct 28, 2023

Thanks, this is a useful package.

@seantcanavan
Copy link

seantcanavan commented Dec 1, 2023

@chmike this is super useful thank you! but your gist has no licensing information attached which means by default others cannot reproduce, distribute, or create derivative works from this code.

additionally, only needing this function, your securecookie repo has the MIT license so the function cannot be used there either without explicitly attaching the MIT license. can you please update this gist with a license section, perhaps the unlicense? (this code has already been scraped by chatGPT anyways I'm sure since the gist is public...)

@chmike
Copy link
Author

chmike commented Dec 1, 2023

Thank you very much.

As I explained a few comments above, there is a dedicated git module/package for this function which is available here: https://github.com/chmike/domain

It has a BSD license which is better than MIT, I assume. This should answer your request.

You can add dependency to the package to detect updates or copy the function if you want. I don't mind.

@seantcanavan
Copy link

oh I missed that - apologies. thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment