indieauth-server/internal/utilities/url_canonicalisation.go

70 lines
1.7 KiB
Go
Raw Normal View History

2024-10-17 23:17:18 +01:00
package utilities
import (
"errors"
"fmt"
"net"
"net/url"
"regexp"
)
const (
httpScheme = "http://"
httpsScheme = "https://"
)
var (
ErrMissingHostname = errors.New("the hostname is missing from the URL")
ErrHostIsIPAddress = errors.New("the hostname is an IP address")
ErrInvalidURLScheme = errors.New("invalid URL scheme")
ErrURLContainsFragment = errors.New("the URL contains a fragment")
ErrURLContainsPort = errors.New("the URL contains a port")
)
// ValidateProfileURL validates the given profile URL according to the indieauth
// specification. ValidateProfileURL returns the canonicalised profile URL after
// validation checks.
func ValidateProfileURL(profileURL string) (string, error) {
// Using regex to get and validate the scheme.
// If its missing then set the scheme to https
pattern := regexp.MustCompile(`^[a-z].*:\/\/|^[a-z].*:`)
scheme := pattern.FindString(profileURL)
if scheme == "" {
profileURL = httpsScheme + profileURL
} else if scheme != httpsScheme && scheme != httpScheme {
return "", ErrInvalidURLScheme
}
parsedProfileURL, err := url.Parse(profileURL)
if err != nil {
return "", fmt.Errorf("unable to parse the URL %q: %w", profileURL, err)
}
if parsedProfileURL.Hostname() == "" {
return "", ErrMissingHostname
}
if ip := net.ParseIP(parsedProfileURL.Hostname()); ip != nil {
return "", ErrHostIsIPAddress
}
if parsedProfileURL.Fragment != "" {
return "", ErrURLContainsFragment
}
if parsedProfileURL.Port() != "" {
return "", ErrURLContainsPort
}
if parsedProfileURL.Scheme == "" {
parsedProfileURL.Scheme = "https"
}
if parsedProfileURL.Path == "" {
parsedProfileURL.Path = "/"
}
return parsedProfileURL.String(), nil
}