@@ -2,6 +2,7 @@ package caddyhttp
22
33import (
44 "context"
5+ "crypto/tls"
56 "errors"
67 "net/http"
78 "net/http/httptest"
@@ -206,9 +207,11 @@ func TestMetricsInstrumentedHandler(t *testing.T) {
206207func TestMetricsInstrumentedHandlerPerHost (t * testing.T ) {
207208 ctx , _ := caddy .NewContext (caddy.Context {Context : context .Background ()})
208209 metrics := & Metrics {
209- PerHost : true ,
210- init : sync.Once {},
211- httpMetrics : & httpMetrics {},
210+ PerHost : true ,
211+ AllowCatchAllHosts : true , // Allow all hosts for testing
212+ init : sync.Once {},
213+ httpMetrics : & httpMetrics {},
214+ allowedHosts : make (map [string ]struct {}),
212215 }
213216 handlerErr := errors .New ("oh noes" )
214217 response := []byte ("hello world!" )
@@ -379,6 +382,112 @@ func TestMetricsInstrumentedHandlerPerHost(t *testing.T) {
379382 }
380383}
381384
385+ func TestMetricsCardinalityProtection (t * testing.T ) {
386+ ctx , _ := caddy .NewContext (caddy.Context {Context : context .Background ()})
387+
388+ // Test 1: Without AllowCatchAllHosts, arbitrary hosts should be mapped to "_other"
389+ metrics := & Metrics {
390+ PerHost : true ,
391+ AllowCatchAllHosts : false , // Default - should map unknown hosts to "_other"
392+ init : sync.Once {},
393+ httpMetrics : & httpMetrics {},
394+ allowedHosts : make (map [string ]struct {}),
395+ }
396+
397+ // Add one allowed host
398+ metrics .allowedHosts ["allowed.com" ] = struct {}{}
399+
400+ mh := middlewareHandlerFunc (func (w http.ResponseWriter , r * http.Request , h Handler ) error {
401+ w .Write ([]byte ("hello" ))
402+ return nil
403+ })
404+
405+ ih := newMetricsInstrumentedHandler (ctx , "test" , mh , metrics )
406+
407+ // Test request to allowed host
408+ r1 := httptest .NewRequest ("GET" , "http://allowed.com/" , nil )
409+ r1 .Host = "allowed.com"
410+ w1 := httptest .NewRecorder ()
411+ ih .ServeHTTP (w1 , r1 , HandlerFunc (func (w http.ResponseWriter , r * http.Request ) error { return nil }))
412+
413+ // Test request to unknown host (should be mapped to "_other")
414+ r2 := httptest .NewRequest ("GET" , "http://attacker.com/" , nil )
415+ r2 .Host = "attacker.com"
416+ w2 := httptest .NewRecorder ()
417+ ih .ServeHTTP (w2 , r2 , HandlerFunc (func (w http.ResponseWriter , r * http.Request ) error { return nil }))
418+
419+ // Test request to another unknown host (should also be mapped to "_other")
420+ r3 := httptest .NewRequest ("GET" , "http://evil.com/" , nil )
421+ r3 .Host = "evil.com"
422+ w3 := httptest .NewRecorder ()
423+ ih .ServeHTTP (w3 , r3 , HandlerFunc (func (w http.ResponseWriter , r * http.Request ) error { return nil }))
424+
425+ // Check that metrics contain:
426+ // - One entry for "allowed.com"
427+ // - One entry for "_other" (aggregating attacker.com and evil.com)
428+ expected := `
429+ # HELP caddy_http_requests_total Counter of HTTP(S) requests made.
430+ # TYPE caddy_http_requests_total counter
431+ caddy_http_requests_total{handler="test",host="_other",server="UNKNOWN"} 2
432+ caddy_http_requests_total{handler="test",host="allowed.com",server="UNKNOWN"} 1
433+ `
434+
435+ if err := testutil .GatherAndCompare (ctx .GetMetricsRegistry (), strings .NewReader (expected ),
436+ "caddy_http_requests_total" ,
437+ ); err != nil {
438+ t .Errorf ("Cardinality protection test failed: %s" , err )
439+ }
440+ }
441+
442+ func TestMetricsHTTPSCatchAll (t * testing.T ) {
443+ ctx , _ := caddy .NewContext (caddy.Context {Context : context .Background ()})
444+
445+ // Test that HTTPS requests allow catch-all even when AllowCatchAllHosts is false
446+ metrics := & Metrics {
447+ PerHost : true ,
448+ AllowCatchAllHosts : false ,
449+ hasHTTPSServer : true , // Simulate having HTTPS servers
450+ init : sync.Once {},
451+ httpMetrics : & httpMetrics {},
452+ allowedHosts : make (map [string ]struct {}), // Empty - no explicitly allowed hosts
453+ }
454+
455+ mh := middlewareHandlerFunc (func (w http.ResponseWriter , r * http.Request , h Handler ) error {
456+ w .Write ([]byte ("hello" ))
457+ return nil
458+ })
459+
460+ ih := newMetricsInstrumentedHandler (ctx , "test" , mh , metrics )
461+
462+ // Test HTTPS request (should be allowed even though not in allowedHosts)
463+ r1 := httptest .NewRequest ("GET" , "https://unknown.com/" , nil )
464+ r1 .Host = "unknown.com"
465+ r1 .TLS = & tls.ConnectionState {} // Mark as TLS/HTTPS
466+ w1 := httptest .NewRecorder ()
467+ ih .ServeHTTP (w1 , r1 , HandlerFunc (func (w http.ResponseWriter , r * http.Request ) error { return nil }))
468+
469+ // Test HTTP request (should be mapped to "_other")
470+ r2 := httptest .NewRequest ("GET" , "http://unknown.com/" , nil )
471+ r2 .Host = "unknown.com"
472+ // No TLS field = HTTP request
473+ w2 := httptest .NewRecorder ()
474+ ih .ServeHTTP (w2 , r2 , HandlerFunc (func (w http.ResponseWriter , r * http.Request ) error { return nil }))
475+
476+ // Check that HTTPS request gets real host, HTTP gets "_other"
477+ expected := `
478+ # HELP caddy_http_requests_total Counter of HTTP(S) requests made.
479+ # TYPE caddy_http_requests_total counter
480+ caddy_http_requests_total{handler="test",host="_other",server="UNKNOWN"} 1
481+ caddy_http_requests_total{handler="test",host="unknown.com",server="UNKNOWN"} 1
482+ `
483+
484+ if err := testutil .GatherAndCompare (ctx .GetMetricsRegistry (), strings .NewReader (expected ),
485+ "caddy_http_requests_total" ,
486+ ); err != nil {
487+ t .Errorf ("HTTPS catch-all test failed: %s" , err )
488+ }
489+ }
490+
382491type middlewareHandlerFunc func (http.ResponseWriter , * http.Request , Handler ) error
383492
384493func (f middlewareHandlerFunc ) ServeHTTP (w http.ResponseWriter , r * http.Request , h Handler ) error {
0 commit comments