interal/api: replace liner array scan with hashmap lookup in domain validation
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: Iac969e7dc6e4ca3f93410fccac1995636a6a6964
This commit is contained in:
parent
987ddd92cc
commit
4e0b8f0d0a
3 changed files with 77 additions and 9 deletions
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"slices"
|
||||||
|
|
||||||
"notashelf.dev/watchdog/internal/limits"
|
"notashelf.dev/watchdog/internal/limits"
|
||||||
)
|
)
|
||||||
|
|
@ -46,13 +47,7 @@ func (e *Event) Validate(allowedDomains []string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if domain is in allowed list
|
// Check if domain is in allowed list
|
||||||
allowed := false
|
allowed := slices.Contains(allowedDomains, e.Domain)
|
||||||
for _, domain := range allowedDomains {
|
|
||||||
if e.Domain == domain {
|
|
||||||
allowed = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !allowed {
|
if !allowed {
|
||||||
return fmt.Errorf("domain not allowed")
|
return fmt.Errorf("domain not allowed")
|
||||||
}
|
}
|
||||||
|
|
@ -76,3 +71,34 @@ func (e *Event) Validate(allowedDomains []string) error {
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ValidateWithMap checks if the event is valid using a domain map (O(1) lookup)
|
||||||
|
func (e *Event) ValidateWithMap(allowedDomains map[string]bool) error {
|
||||||
|
if e.Domain == "" {
|
||||||
|
return fmt.Errorf("domain required")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if domain is in allowed map (O(1) instead of O(n))
|
||||||
|
if !allowedDomains[e.Domain] {
|
||||||
|
return fmt.Errorf("domain not allowed")
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.Path == "" {
|
||||||
|
return fmt.Errorf("path required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(e.Path) > limits.MaxPathLen {
|
||||||
|
return fmt.Errorf("path too long")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(e.Referrer) > limits.MaxRefLen {
|
||||||
|
return fmt.Errorf("referrer too long")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate screen width is in reasonable range
|
||||||
|
if e.Width < 0 || e.Width > limits.MaxWidth {
|
||||||
|
return fmt.Errorf("invalid width")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -230,3 +230,37 @@ func TestValidateEvent(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func BenchmarkValidate_SliceLookup(b *testing.B) {
|
||||||
|
// Simulate multi-site with 50 domains
|
||||||
|
domains := make([]string, 50)
|
||||||
|
for i := range 50 {
|
||||||
|
domains[i] = strings.Repeat("site", i) + ".com"
|
||||||
|
}
|
||||||
|
|
||||||
|
event := Event{
|
||||||
|
Domain: domains[49], // Worst case - last in list
|
||||||
|
Path: "/test",
|
||||||
|
}
|
||||||
|
|
||||||
|
for b.Loop() {
|
||||||
|
_ = event.Validate(domains)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkValidate_MapLookup(b *testing.B) {
|
||||||
|
// Simulate multi-site with 50 domains
|
||||||
|
domainMap := make(map[string]bool, 50)
|
||||||
|
for i := range 50 {
|
||||||
|
domainMap[strings.Repeat("site", i)+".com"] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
event := Event{
|
||||||
|
Domain: strings.Repeat("site", 49) + ".com", // any position
|
||||||
|
Path: "/test",
|
||||||
|
}
|
||||||
|
|
||||||
|
for b.Loop() {
|
||||||
|
_ = event.ValidateWithMap(domainMap)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import (
|
||||||
// Handles incoming analytics events
|
// Handles incoming analytics events
|
||||||
type IngestionHandler struct {
|
type IngestionHandler struct {
|
||||||
cfg *config.Config
|
cfg *config.Config
|
||||||
|
domainMap map[string]bool // O(1) domain validation
|
||||||
pathNorm *normalize.PathNormalizer
|
pathNorm *normalize.PathNormalizer
|
||||||
pathRegistry *aggregate.PathRegistry
|
pathRegistry *aggregate.PathRegistry
|
||||||
refRegistry *normalize.ReferrerRegistry
|
refRegistry *normalize.ReferrerRegistry
|
||||||
|
|
@ -40,8 +41,15 @@ func NewIngestionHandler(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Build domain map for O(1) validation
|
||||||
|
domainMap := make(map[string]bool, len(cfg.Site.Domains))
|
||||||
|
for _, domain := range cfg.Site.Domains {
|
||||||
|
domainMap[domain] = true
|
||||||
|
}
|
||||||
|
|
||||||
return &IngestionHandler{
|
return &IngestionHandler{
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
|
domainMap: domainMap,
|
||||||
pathNorm: pathNorm,
|
pathNorm: pathNorm,
|
||||||
pathRegistry: pathRegistry,
|
pathRegistry: pathRegistry,
|
||||||
refRegistry: refRegistry,
|
refRegistry: refRegistry,
|
||||||
|
|
@ -95,8 +103,8 @@ func (h *IngestionHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate event
|
// Validate event via map lookup (also O(1))
|
||||||
if err := event.Validate(h.cfg.Site.Domains); err != nil {
|
if err := event.ValidateWithMap(h.domainMap); err != nil {
|
||||||
http.Error(w, "Bad request", http.StatusBadRequest)
|
http.Error(w, "Bad request", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue