Skip to content

Commit 698e01b

Browse files
Zero allocation fixes + utils
1 parent 4b45ec9 commit 698e01b

File tree

4 files changed

+64
-14
lines changed

4 files changed

+64
-14
lines changed

docs/middleware/hostauthorization.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -62,20 +62,20 @@ AllowedHosts: []string{"myapp.com", ".myapp.com"},
6262

6363
### CIDR Ranges
6464

65-
Useful for internal services that should only respond to requests from known IP ranges:
65+
Useful for services accessed directly by IP (e.g. internal tooling) where the `Host` header will be a raw IP address. This matches the **Host header value** against a CIDR range — it does not filter by client IP address:
6666

6767
```go
6868
app.Use(hostauthorization.New(hostauthorization.Config{
6969
AllowedHosts: []string{
7070
"internal.myapp.com",
71-
"10.0.0.0/8", // internal network
72-
"127.0.0.1", // localhost
71+
"10.0.0.0/8", // Host header IPs in this range are allowed
72+
"127.0.0.1", // Host header must be exactly this IP
7373
},
7474
}))
7575

7676
// Host: internal.myapp.com → 200 OK
77-
// Host: 10.0.50.3 → 200 OK
78-
// Host: 169.254.169.254 → 403 Forbidden (blocks cloud metadata)
77+
// Host: 10.0.50.3 → 200 OK (Host header IP is in 10.0.0.0/8)
78+
// Host: 169.254.169.254 → 403 Forbidden (Host header IP not in allowlist)
7979
```
8080

8181
### Skipping Health Checks

middleware/hostauthorization/config.go

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,11 @@ type Config struct {
4444
var ConfigDefault = Config{}
4545

4646
func configDefault(config ...Config) Config {
47-
if len(config) < 1 {
48-
panic("hostauthorization: AllowedHosts or AllowedHostsFunc is required")
47+
cfg := ConfigDefault
48+
if len(config) > 0 {
49+
cfg = config[0]
4950
}
5051

51-
cfg := config[0]
52-
5352
if len(cfg.AllowedHosts) == 0 && cfg.AllowedHostsFunc == nil {
5453
panic("hostauthorization: AllowedHosts or AllowedHostsFunc is required")
5554
}

middleware/hostauthorization/hostauthorization.go

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import (
55
"strings"
66

77
"github.com/gofiber/fiber/v3"
8+
"github.com/gofiber/utils/v2"
9+
utilsstrings "github.com/gofiber/utils/v2/strings"
810
)
911

1012
// parsedHosts holds the pre-parsed host matching structures.
@@ -22,7 +24,11 @@ func parseAllowedHosts(hosts []string) parsedHosts {
2224
}
2325

2426
for _, h := range hosts {
25-
h = strings.ToLower(strings.TrimSpace(h))
27+
h = utils.TrimSpace(h)
28+
if h == "" {
29+
continue
30+
}
31+
h = normalizeHost(h)
2632
if h == "" {
2733
continue
2834
}
@@ -37,8 +43,8 @@ func parseAllowedHosts(hosts []string) parsedHosts {
3743
parsed.cidrNets = append(parsed.cidrNets, cidr)
3844

3945
case strings.HasPrefix(h, "."):
40-
// Subdomain wildcard — store without leading dot
41-
parsed.wildcardSuffixes = append(parsed.wildcardSuffixes, h[1:])
46+
// Subdomain wildcard — store with leading dot to avoid allocation in hot path
47+
parsed.wildcardSuffixes = append(parsed.wildcardSuffixes, h)
4248

4349
default:
4450
// Exact match
@@ -59,7 +65,7 @@ func normalizeHost(host string) string {
5965
// Strip trailing dot (FQDN normalization)
6066
host = strings.TrimSuffix(host, ".")
6167

62-
return strings.ToLower(host)
68+
return utilsstrings.ToLower(host)
6369
}
6470

6571
// matchHost checks if the given host matches any of the parsed allowed hosts.
@@ -71,7 +77,7 @@ func matchHost(host string, parsed parsedHosts, allowedHostsFunc func(string) bo
7177

7278
// Subdomain wildcard: ".myapp.com" matches "api.myapp.com" but NOT "myapp.com"
7379
for _, suffix := range parsed.wildcardSuffixes {
74-
if strings.HasSuffix(host, "."+suffix) {
80+
if strings.HasSuffix(host, suffix) {
7581
return true
7682
}
7783
}

middleware/hostauthorization/hostauthorization_test.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -586,6 +586,51 @@ func Test_HostAuthorization_XForwardedHost_NoTrustProxy(t *testing.T) {
586586

587587
// --- Benchmarks ---
588588

589+
// --- Low-level matchHost benchmarks (isolate matching cost from HTTP pipeline) ---
590+
591+
func Benchmark_matchHost_ExactMatch(b *testing.B) {
592+
parsed := parseAllowedHosts([]string{"example.com"})
593+
b.ReportAllocs()
594+
b.ResetTimer()
595+
for i := 0; i < b.N; i++ {
596+
matchHost("example.com", parsed, nil)
597+
}
598+
}
599+
600+
func Benchmark_matchHost_WildcardMatch(b *testing.B) {
601+
parsed := parseAllowedHosts([]string{".myapp.com"})
602+
b.ReportAllocs()
603+
b.ResetTimer()
604+
for i := 0; i < b.N; i++ {
605+
matchHost("api.myapp.com", parsed, nil)
606+
}
607+
}
608+
609+
func Benchmark_matchHost_CIDRMatch(b *testing.B) {
610+
parsed := parseAllowedHosts([]string{"10.0.0.0/8"})
611+
b.ReportAllocs()
612+
b.ResetTimer()
613+
for i := 0; i < b.N; i++ {
614+
matchHost("10.0.50.3", parsed, nil)
615+
}
616+
}
617+
618+
func Benchmark_matchHost_Mixed(b *testing.B) {
619+
parsed := parseAllowedHosts([]string{
620+
"example.com",
621+
".myapp.com",
622+
"10.0.0.0/8",
623+
"127.0.0.1",
624+
})
625+
b.ReportAllocs()
626+
b.ResetTimer()
627+
for i := 0; i < b.N; i++ {
628+
matchHost("api.myapp.com", parsed, nil)
629+
}
630+
}
631+
632+
// --- Full HTTP pipeline benchmarks (includes app.Test() overhead) ---
633+
589634
func Benchmark_HostAuthorization_ExactMatch(b *testing.B) {
590635
app := fiber.New()
591636
app.Use(New(Config{

0 commit comments

Comments
 (0)