Files
bucketeer/internal/classify/classify.go
T

198 lines
5.4 KiB
Go
Raw Normal View History

// Package classify maps a DNS hostname to a cloud/database/storage resource.
//
// The hot path is deliberately allocation-free: it never lowercases unless an
// uppercase byte is present, never splits the hostname into a []string, never
// uses regexp/JSON/reflection, and returns substrings of the input as the
// region/resource. See BenchmarkClassify* for the 0 allocs/op assertion.
package classify
import (
"strings"
"tea.karlcom.de/karl/bucketeer/internal/model"
)
const maxLabels = 64
// scanner holds the byte offsets of each dot-separated label of a hostname.
// It is stack allocated by Classify and never heap allocated.
type scanner struct {
host string
n int
start [maxLabels]int32
}
func newScanner(host string) scanner {
var s scanner
s.host = host
start := int32(0)
for i := 0; i < len(host); i++ {
if host[i] == '.' {
if s.n >= maxLabels {
s.n = -1
return s
}
s.start[s.n] = start
s.n++
start = int32(i + 1)
}
}
if s.n >= maxLabels {
s.n = -1
return s
}
s.start[s.n] = start
s.n++
return s
}
// label returns the i-th label (0 = leftmost). Callers must ensure i < n.
func (s *scanner) label(i int) string {
end := int32(len(s.host))
if i+1 < s.n {
end = s.start[i+1] - 1
}
return s.host[s.start[i]:end]
}
func hasUpper(s string) bool {
for i := 0; i < len(s); i++ {
if s[i] >= 'A' && s[i] <= 'Z' {
return true
}
}
return false
}
func trimTrailingDots(s string) string {
for len(s) > 0 && s[len(s)-1] == '.' {
s = s[:len(s)-1]
}
return s
}
func validHost(host string) bool {
if host == "" || len(host) > 253 {
return false
}
for i := 0; i < len(host); i++ {
c := host[i]
switch {
case c >= 'a' && c <= 'z':
case c >= '0' && c <= '9':
case c == '-' || c == '.' || c == '_':
default:
return false
}
}
return true
}
// Classify returns the provider resource described by host, if any.
// The returned Match's string fields alias host.
func Classify(host string) (model.Match, bool) {
host = trimTrailingDots(host)
if host == "" || len(host) > 253 {
return model.Match{}, false
}
// Wildcard certificates never describe a concrete resource.
if host[0] == '*' {
return model.Match{}, false
}
if hasUpper(host) {
host = strings.ToLower(host)
}
if !validHost(host) {
return model.Match{}, false
}
s := newScanner(host)
if s.n < 2 {
return model.Match{}, false
}
switch {
case strings.HasSuffix(host, ".amazonaws.com"):
return classifyAWS(&s)
case strings.HasSuffix(host, ".core.windows.net"):
return classifyAzureCore(&s)
case strings.HasSuffix(host, ".database.windows.net"):
return classifyAzureDatabase(&s)
case strings.HasSuffix(host, ".database.azure.com"):
return classifyAzureFlexibleServer(&s)
case strings.HasSuffix(host, ".documents.azure.com"):
return classifyAzureDocuments(&s)
case strings.HasSuffix(host, ".cosmos.azure.com"):
return classifyAzureCosmos(&s)
case strings.HasSuffix(host, ".cache.windows.net"):
return classifyAzureRedis(&s)
case strings.HasSuffix(host, ".redis.azure.net"):
return classifyAzureManagedRedis(&s)
case strings.HasSuffix(host, ".search.windows.net"):
return classifyAzureSearch(&s)
case strings.HasSuffix(host, ".storage.googleapis.com"):
return classifyGCS(&s)
case strings.HasSuffix(host, ".firebaseio.com"):
return classifyFirebaseIO(&s)
case strings.HasSuffix(host, ".firebasedatabase.app"):
return classifyFirebaseDB(&s)
case strings.HasSuffix(host, ".appspot.com"):
return classifyAppspot(&s)
case strings.HasSuffix(host, ".r2.cloudflarestorage.com"):
return classifyR2(&s)
case strings.HasSuffix(host, ".r2.dev"):
return classifyR2Dev(&s)
case strings.HasSuffix(host, ".digitaloceanspaces.com"):
return classifyDigitalOceanSpaces(&s)
case strings.HasSuffix(host, ".ondigitalocean.com"):
return classifyDigitalOceanDB(&s)
case strings.HasSuffix(host, ".supabase.co"):
return classifySupabase(&s)
case strings.HasSuffix(host, ".supabase.com"):
return classifySupabasePooler(&s)
case strings.HasSuffix(host, ".customer-oci.com"):
return classifyOCIResource(&s)
case strings.HasSuffix(host, ".oraclecloud.com"):
return classifyOCIGeneric(&s)
}
return model.Match{}, false
}
// ShouldEnrich reports whether a non-matching hostname is worth a DNS/CNAME
// lookup. It rejects wildcards, the certspotter unparsable placeholder, single
// labels and obvious provider service apexes.
func ShouldEnrich(host string) bool {
host = trimTrailingDots(host)
if len(host) < 3 || host[0] == '*' {
return false
}
if host[0] == '<' { // certspotter's "<unparsable>" placeholder
return false
}
if !strings.Contains(host, ".") {
return false
}
if hasUpper(host) {
host = strings.ToLower(host)
}
// Provider service apexes never yield a customer resource via CNAME.
switch {
case host == "amazonaws.com", strings.HasSuffix(host, ".amazonaws.com"):
return false
case host == "core.windows.net", strings.HasSuffix(host, ".windows.net"):
return false
case host == "azure.com", strings.HasSuffix(host, ".azure.com"):
return false
case host == "googleapis.com", strings.HasSuffix(host, ".googleapis.com"):
return false
case host == "cloudflarestorage.com", host == "r2.dev":
return false
case strings.HasSuffix(host, ".digitaloceanspaces.com"):
return false
case strings.HasSuffix(host, ".supabase.co"), strings.HasSuffix(host, ".supabase.com"):
return false
case strings.HasSuffix(host, ".oraclecloud.com"), strings.HasSuffix(host, ".customer-oci.com"):
return false
}
return true
}