Files
warmbly/internal/utils/validate/tracking.go
T

57 lines
1.6 KiB
Go

package validate
import (
"net"
"strings"
)
// TrackingHostname reports whether host is usable as a tracking domain: a bare
// hostname with no scheme, no port, no path, no raw IP literal and no
// internal/metadata name, mirroring the webhook-SSRF posture. Mailbox and
// campaign tracking domains share one definition so a host that is valid on one
// cannot be invalid on the other.
func TrackingHostname(host string) bool {
if host == "" || len(host) > 253 {
return false
}
if strings.Contains(host, "://") || strings.ContainsAny(host, " \t\r\n/\\?#@:") {
return false
}
if net.ParseIP(host) != nil {
return false
}
lower := strings.ToLower(host)
if lower == "localhost" || strings.HasSuffix(lower, ".localhost") || lower == "metadata.google.internal" {
return false
}
// A single label has no public zone to own, and a trailing dot is a
// fully-qualified form nobody types into this field.
if !strings.Contains(lower, ".") || strings.HasPrefix(lower, ".") || strings.HasSuffix(lower, ".") {
return false
}
for _, label := range strings.Split(lower, ".") {
if label == "" || len(label) > 63 {
return false
}
if strings.HasPrefix(label, "-") || strings.HasSuffix(label, "-") {
return false
}
for _, c := range label {
if !((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-') {
return false
}
}
}
// The last label is the TLD: letters only, at least two of them.
tld := lower[strings.LastIndex(lower, ".")+1:]
if len(tld) < 2 {
return false
}
for _, c := range tld {
if c < 'a' || c > 'z' {
return false
}
}
return true
}