mirror of
https://github.com/steveiliop56/tinyauth.git
synced 2026-07-13 21:41:12 +00:00
161 lines
4.1 KiB
Go
161 lines
4.1 KiB
Go
// Package validators provides validators for various types of data.
|
|
//
|
|
// Domain validator is a simple utility that ensures two domains are exact
|
|
// matches while ensuring that techniques used to bypass such checks do
|
|
// not impact the validation.
|
|
|
|
package validators
|
|
|
|
import (
|
|
"fmt"
|
|
"net"
|
|
"net/url"
|
|
"slices"
|
|
"strings"
|
|
|
|
"golang.org/x/net/idna"
|
|
)
|
|
|
|
// DomainValidatorOptions is a set of options for DomainValidator.
|
|
type DomainValidatorOptions struct {
|
|
// Ensure domains have the same scheme.
|
|
WithScheme bool
|
|
// Ensure domains have the same port.
|
|
WithPort bool
|
|
// Specify a list of allowed schemes IF WithScheme is set to true.
|
|
// Leave empty to allow any scheme.
|
|
AllowedSchemes []string
|
|
}
|
|
|
|
// DomainValidator is a simple utility that ensures two domains are exact
|
|
// matches while ensuring that techniques used to bypass such checks do
|
|
// not impact the validation.
|
|
type DomainValidator struct {
|
|
opts DomainValidatorOptions
|
|
}
|
|
|
|
// NewDomainValidator creates a new DomainValidator.
|
|
func NewDomainValidator(opts DomainValidatorOptions) *DomainValidator {
|
|
return &DomainValidator{
|
|
opts: opts,
|
|
}
|
|
}
|
|
|
|
func (v *DomainValidator) getURL(i string) (*url.URL, error) {
|
|
u, err := url.Parse(i)
|
|
|
|
if !v.opts.WithScheme && (err != nil || u.Host == "") {
|
|
u, err = url.Parse("tinyauth://" + i)
|
|
}
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse input url: %w", err)
|
|
}
|
|
|
|
if u.Host == "" {
|
|
return nil, fmt.Errorf("input url is invalid")
|
|
}
|
|
|
|
if v.opts.WithPort && !v.opts.WithScheme && u.Port() == "" {
|
|
return nil, fmt.Errorf("port validation is enabled but port is missing in input url and schemes are not enabled")
|
|
}
|
|
|
|
if v.opts.WithScheme {
|
|
// Empty scheme means that we parsed the url with the tinyauth:// placeholder
|
|
if u.Scheme == "tinyauth" {
|
|
return nil, fmt.Errorf("input url is missing scheme")
|
|
}
|
|
if len(v.opts.AllowedSchemes) > 0 && !slices.Contains(v.opts.AllowedSchemes, u.Scheme) {
|
|
return nil, fmt.Errorf("scheme %s not allowed", u.Scheme)
|
|
}
|
|
}
|
|
|
|
return u, nil
|
|
}
|
|
|
|
func (v *DomainValidator) getEffectivePort(u *url.URL) string {
|
|
if u.Port() != "" {
|
|
return u.Port()
|
|
}
|
|
if u.Scheme == "https" {
|
|
return "443"
|
|
}
|
|
return "80"
|
|
}
|
|
|
|
func (v *DomainValidator) formatHostname(hostname string) (string, error) {
|
|
hostname = strings.ToLower(hostname)
|
|
hostname = strings.TrimSuffix(hostname, ".")
|
|
if net.ParseIP(hostname) != nil {
|
|
return "", fmt.Errorf("ip addresses are not supported")
|
|
}
|
|
hostname, err := idna.Lookup.ToASCII(hostname)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to convert hostname to ascii: %w", err)
|
|
}
|
|
return hostname, nil
|
|
}
|
|
|
|
// Validate ensures that two domains are exact matches with the
|
|
// options defined in the DomainValidatorOptions. It ensures that the
|
|
// inputs are proper URLs and contain a host. It lowercases the hostnames
|
|
// and removes the trailing dot. Finally, it checks that the hostnames are
|
|
// equal unless WithScheme or WithPort is set to true where it also
|
|
// validates the scheme and port respectively.
|
|
func (v *DomainValidator) Validate(expected, actual string) error {
|
|
eu, err := v.getURL(expected)
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
au, err := v.getURL(actual)
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if v.opts.WithScheme {
|
|
if eu.Scheme != au.Scheme {
|
|
return fmt.Errorf("expected scheme %s, got %s", eu.Scheme, au.Scheme)
|
|
}
|
|
}
|
|
|
|
if v.opts.WithPort {
|
|
if v.getEffectivePort(eu) != v.getEffectivePort(au) {
|
|
return fmt.Errorf("expected port %s, got %s", v.getEffectivePort(eu), v.getEffectivePort(au))
|
|
}
|
|
}
|
|
|
|
euf, err := v.formatHostname(eu.Hostname())
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
auf, err := v.formatHostname(au.Hostname())
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if euf != auf {
|
|
return fmt.Errorf("expected hostname %s, got %s", euf, auf)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// SafeHostname uses the internal validation for domains that Validator uses
|
|
// to parse a hostname. It ensures the input URL is a valid URL, that a host
|
|
// is present and that the hostname is lowercased and without a trailing dot.
|
|
func (v *DomainValidator) SafeHostname(input string) (string, error) {
|
|
u, err := v.getURL(input)
|
|
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return v.formatHostname(u.Hostname())
|
|
}
|