77 "net/url"
88 "strings"
99
10+ "github.com/gofiber/utils/v2"
1011 utilsstrings "github.com/gofiber/utils/v2/strings"
1112)
1213
@@ -15,76 +16,120 @@ const (
1516 schemeHTTPS = "https"
1617)
1718
19+ // schemePorts is the single source of truth for the schemes whose default
20+ // port is normalized away during origin comparison.
21+ var schemePorts = [... ]struct {
22+ scheme string
23+ port string
24+ }{
25+ {schemeHTTP , "80" },
26+ {schemeHTTPS , "443" },
27+ }
28+
29+ // foldSchemePort resolves scheme against schemePorts, ASCII
30+ // case-insensitively, returning the canonical lowercase scheme and its
31+ // default port.
32+ func foldSchemePort (scheme string ) (canonical , port string , known bool ) { //nolint:nonamedreturns // names document the three results
33+ for _ , e := range schemePorts {
34+ if utils .EqualFold (scheme , e .scheme ) {
35+ return e .scheme , e .port , true
36+ }
37+ }
38+ return "" , "" , false
39+ }
40+
1841// Match reports whether (schemeA, hostA) and (schemeB, hostB) denote the same
1942// origin. Scheme comparison is case-insensitive and default ports (http:80,
2043// https:443) are normalized so "example.com" and "example.com:443" match.
2144func Match (schemeA , hostA , schemeB , hostB string ) bool {
22- normalizedSchemeA := utilsstrings .ToLower (schemeA )
23- normalizedSchemeB := utilsstrings .ToLower (schemeB )
45+ if ! utils .EqualFold (schemeA , schemeB ) {
46+ return false
47+ }
48+
49+ // Identical host strings always normalize identically, so they denote the
50+ // same origin once the schemes match. This is the dominant same-origin
51+ // input (e.g. Origin-vs-Host on non-CORS requests).
52+ if hostA == hostB {
53+ return true
54+ }
2455
25- normalizedHostA := normalizeSchemeHost (normalizedSchemeA , hostA )
26- normalizedHostB := normalizeSchemeHost (normalizedSchemeB , hostB )
56+ scheme , defaultPort , known := foldSchemePort (schemeA )
57+ if ! known {
58+ // Unknown schemes get no port normalization: the hosts must simply be
59+ // equal, ASCII case-insensitively.
60+ return utils .EqualFold (hostA , hostB )
61+ }
62+
63+ // Fast path for two clean "host" or "host:port" values (the common case):
64+ // compare the host parts case-insensitively and the effective ports
65+ // exactly, without allocating lowered or port-normalized copies.
66+ if hostOnlyA , portA , cleanA := splitCleanHostPort (hostA ); cleanA {
67+ if hostOnlyB , portB , cleanB := splitCleanHostPort (hostB ); cleanB {
68+ if portA == "" {
69+ portA = defaultPort
70+ }
71+ if portB == "" {
72+ portB = defaultPort
73+ }
74+ return portA == portB && utils .EqualFold (hostOnlyA , hostOnlyB )
75+ }
76+ }
2777
28- return normalizedSchemeA == normalizedSchemeB && normalizedHostA == normalizedHostB
78+ // Anything unusual (userinfo, percent-encoding, bracketed IPv6, control
79+ // chars, invalid port, ...) takes the legacy normalize-and-compare path.
80+ return normalizeHostPort (scheme , hostA , defaultPort ) == normalizeHostPort (scheme , hostB , defaultPort )
2981}
3082
31- func normalizeSchemeHost (scheme , host string ) string {
83+ // normalizeHostPort lowercases host and appends defaultPort when no explicit
84+ // port is present. scheme is only used by the url.Parse fallback.
85+ func normalizeHostPort (scheme , host , defaultPort string ) string {
3286 host = utilsstrings .ToLower (host )
3387
34- var defaultPort string
35- switch scheme {
36- case schemeHTTP :
37- defaultPort = "80"
38- case schemeHTTPS :
39- defaultPort = "443"
40- default :
41- return host
42- }
43-
44- // Fast path for a clean "host" or "host:port" value (the common case),
45- // avoiding the url.Parse allocation. Anything unusual (userinfo, path,
46- // percent-encoding, bracketed IPv6, control chars, empty/invalid port, ...)
47- // falls back to the url.Parse path, which preserves the exact legacy behavior.
48- if hasPort , clean := classifyHostPort (host ); clean {
49- if hasPort {
88+ // Clean "host" or "host:port" values (e.g. the clean side of a mixed
89+ // clean/unclean pair; Match handles the clean/clean case itself) avoid the
90+ // url.Parse allocation. Anything unusual (userinfo, path, percent-encoding,
91+ // bracketed IPv6, control chars, empty/invalid port, ...) falls back to the
92+ // url.Parse path, which preserves the exact legacy behavior.
93+ if _ , port , clean := splitCleanHostPort (host ); clean {
94+ if port != "" {
5095 return host
5196 }
5297 return host + ":" + defaultPort
5398 }
5499
55- return normalizeSchemeHostViaParse (scheme , host , defaultPort )
100+ return normalizeHostPortViaParse (scheme , host , defaultPort )
56101}
57102
58- // classifyHostPort reports whether host is a plain "<reg-name-or-IPv4>" or
59- // "<reg-name-or-IPv4>:<port>" value (clean) and, if so, whether it carries an
60- // explicit numeric port. The accepted character set is deliberately narrow
61- // (lowercase ASCII letters, digits, '.', ' -', and a single ':'); anything else,
62- // including bracketed IPv6 literals, returns clean=false and is handled by the
103+ // splitCleanHostPort splits a plain "<reg-name-or-IPv4>" or
104+ // "<reg-name-or-IPv4>:<port>" value (clean) into its host and port parts. The
105+ // accepted character set is deliberately narrow (ASCII letters, digits, '.',
106+ // ' -', and a single ':' followed by digits ); anything else, including
107+ // bracketed IPv6 literals, returns clean=false and is handled by the
63108// url.Parse fallback so behavior stays identical to the legacy implementation.
64- func classifyHostPort ( host string ) (hasPort , clean bool ) { //nolint:nonamedreturns // names document the two booleans
109+ func splitCleanHostPort ( s string ) (host , port string , clean bool ) { //nolint:nonamedreturns // names document the three results
65110 colon := - 1
66- for i := 0 ; i < len (host ); i ++ {
67- c := host [i ]
111+ for i := 0 ; i < len (s ); i ++ {
112+ c := s [i ]
68113 switch {
69- case c >= 'a' && c <= 'z' , c >= '0' && c <= '9' , c == '.' , c == '-' :
114+ case c >= 'a' && c <= 'z' , c >= 'A' && c <= 'Z' , c >= ' 0' && c <= '9' , c == '.' , c == '-' :
70115 // safe reg-name / IPv4 character
71116 case c == ':' :
72117 if colon >= 0 {
73- return false , false // more than one colon -> not a clean host:port
118+ return "" , "" , false // more than one colon -> not a clean host:port
74119 }
75120 colon = i
76121 default :
77- return false , false // brackets, control chars, anything else
122+ return "" , "" , false // brackets, control chars, anything else
78123 }
79124 }
80125
81126 if colon < 0 {
82- return false , host != "" // no port; empty host falls back to url.Parse
127+ return s , "" , s != "" // no port; empty host falls back to url.Parse
83128 }
84- if ! allDigits (host [colon + 1 :]) {
85- return false , false // "host:" or "host:abc" -> let url.Parse decide
129+ if ! allDigits (s [colon + 1 :]) {
130+ return "" , "" , false // "host:" or "host:abc" -> let url.Parse decide
86131 }
87- return true , true
132+ return s [: colon ], s [ colon + 1 :] , true
88133}
89134
90135// allDigits reports whether s is non-empty and all ASCII digits.
@@ -100,9 +145,9 @@ func allDigits(s string) bool {
100145 return true
101146}
102147
103- // normalizeSchemeHostViaParse is the url.Parse-based fallback. host is already
148+ // normalizeHostPortViaParse is the url.Parse-based fallback. host is already
104149// lowercased and scheme is known to be http or https.
105- func normalizeSchemeHostViaParse (scheme , host , defaultPort string ) string {
150+ func normalizeHostPortViaParse (scheme , host , defaultPort string ) string {
106151 parsedHost , err := url .Parse (scheme + "://" + host )
107152 if err != nil {
108153 return host
0 commit comments