mirror of
https://github.com/pgsty/minio.git
synced 2026-10-31 04:50:40 +03:00
fix: make bucket CORS replication converge
Define a deterministic CORS replication register with durable tombstones, strict source timestamps, equal-time conflict ordering, full-state status, and heal convergence. Serialize local and peer CORS transitions with a distributed namespace lock, validate canonical transport payloads, preserve initial-sync deletes, and fail closed on metadata errors. Add adversarial, concurrent, restart, status, heal, signed admin-dispatch, protocol, and middleware coverage together with the reviewed site-replication design record. Refs #75 Signed-off-by: Feng Ruohang <rh@vonng.com>
This commit is contained in:
@@ -87,6 +87,12 @@ func (c *Config) Validate() error {
|
||||
return errors.New("CORSRule must contain at least one AllowedMethod")
|
||||
}
|
||||
for _, o := range r.AllowedOrigins {
|
||||
if o == "" {
|
||||
return errors.New("AllowedOrigin must not be empty")
|
||||
}
|
||||
if strings.Contains(o, "?") {
|
||||
return errors.New("AllowedOrigin may not contain wildcard '?': " + o)
|
||||
}
|
||||
if strings.Count(o, "*") > 1 {
|
||||
return errors.New("AllowedOrigin may contain at most one wildcard '*': " + o)
|
||||
}
|
||||
@@ -97,6 +103,9 @@ func (c *Config) Validate() error {
|
||||
}
|
||||
}
|
||||
for _, h := range r.AllowedHeaders {
|
||||
if strings.Contains(h, "?") {
|
||||
return errors.New("AllowedHeader may not contain wildcard '?': " + h)
|
||||
}
|
||||
if strings.Count(h, "*") > 1 {
|
||||
return errors.New("AllowedHeader may contain at most one wildcard '*': " + h)
|
||||
}
|
||||
@@ -108,14 +117,19 @@ func (c *Config) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// HasAllowedOrigin reports whether the rule allows the given origin.
|
||||
func (r Rule) HasAllowedOrigin(origin string) bool {
|
||||
func (r Rule) matchAllowedOrigin(origin string) (string, bool) {
|
||||
for _, o := range r.AllowedOrigins {
|
||||
if o == "*" || wildcard.MatchSimple(o, origin) {
|
||||
return true
|
||||
return o, true
|
||||
}
|
||||
}
|
||||
return false
|
||||
return "", false
|
||||
}
|
||||
|
||||
// HasAllowedOrigin reports whether the rule allows the given origin.
|
||||
func (r Rule) HasAllowedOrigin(origin string) bool {
|
||||
_, ok := r.matchAllowedOrigin(origin)
|
||||
return ok
|
||||
}
|
||||
|
||||
// HasAllowedMethod reports whether the rule allows the given HTTP method.
|
||||
@@ -154,15 +168,17 @@ func (r Rule) headerAllowed(header string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// MatchRule returns the first rule whose origin and method both match.
|
||||
func (c *Config) MatchRule(origin, method string) (*Rule, bool) {
|
||||
// MatchRule returns the first rule whose origin and method both match, along
|
||||
// with the configured origin pattern that matched.
|
||||
func (c *Config) MatchRule(origin, method string) (rule *Rule, allowedOrigin string, ok bool) {
|
||||
for i := range c.CORSRules {
|
||||
r := &c.CORSRules[i]
|
||||
if r.HasAllowedOrigin(origin) && r.HasAllowedMethod(method) {
|
||||
return r, true
|
||||
matchedOrigin, originOK := r.matchAllowedOrigin(origin)
|
||||
if originOK && r.HasAllowedMethod(method) {
|
||||
return r, matchedOrigin, true
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
return nil, "", false
|
||||
}
|
||||
|
||||
// MatchPreflight returns the first rule whose origin and method match and
|
||||
@@ -170,17 +186,18 @@ func (c *Config) MatchRule(origin, method string) (*Rule, bool) {
|
||||
// this keeps evaluating subsequent rules until one fully satisfies the
|
||||
// preflight request, since an earlier origin/method match with a more
|
||||
// restrictive header list must not shadow a later, more permissive rule.
|
||||
func (c *Config) MatchPreflight(origin, method string, reqHeaders []string) (rule *Rule, allowedHeaders []string, ok bool) {
|
||||
func (c *Config) MatchPreflight(origin, method string, reqHeaders []string) (rule *Rule, allowedOrigin string, allowedHeaders []string, ok bool) {
|
||||
for i := range c.CORSRules {
|
||||
r := &c.CORSRules[i]
|
||||
if !r.HasAllowedOrigin(origin) || !r.HasAllowedMethod(method) {
|
||||
matchedOrigin, originOK := r.matchAllowedOrigin(origin)
|
||||
if !originOK || !r.HasAllowedMethod(method) {
|
||||
continue
|
||||
}
|
||||
allowed, headersOK := r.FilterAllowedHeaders(reqHeaders)
|
||||
if !headersOK {
|
||||
continue
|
||||
}
|
||||
return r, allowed, true
|
||||
return r, matchedOrigin, allowed, true
|
||||
}
|
||||
return nil, nil, false
|
||||
return nil, "", nil, false
|
||||
}
|
||||
|
||||
@@ -55,10 +55,13 @@ func TestValidateRejections(t *testing.T) {
|
||||
cases := map[string]string{
|
||||
"bad method": `<CORSConfiguration><CORSRule><AllowedOrigin>*</AllowedOrigin><AllowedMethod>TRACE</AllowedMethod></CORSRule></CORSConfiguration>`,
|
||||
"no origin": `<CORSConfiguration><CORSRule><AllowedMethod>GET</AllowedMethod></CORSRule></CORSConfiguration>`,
|
||||
"empty origin": `<CORSConfiguration><CORSRule><AllowedOrigin></AllowedOrigin><AllowedMethod>GET</AllowedMethod></CORSRule></CORSConfiguration>`,
|
||||
"no method": `<CORSConfiguration><CORSRule><AllowedOrigin>*</AllowedOrigin></CORSRule></CORSConfiguration>`,
|
||||
"negative age": `<CORSConfiguration><CORSRule><AllowedOrigin>*</AllowedOrigin><AllowedMethod>GET</AllowedMethod><MaxAgeSeconds>-1</MaxAgeSeconds></CORSRule></CORSConfiguration>`,
|
||||
"multi wildcard origin": `<CORSConfiguration><CORSRule><AllowedOrigin>https://*.*.example.com</AllowedOrigin><AllowedMethod>GET</AllowedMethod></CORSRule></CORSConfiguration>`,
|
||||
"multi wildcard header": `<CORSConfiguration><CORSRule><AllowedOrigin>*</AllowedOrigin><AllowedMethod>GET</AllowedMethod><AllowedHeader>x-*-*</AllowedHeader></CORSRule></CORSConfiguration>`,
|
||||
"question mark origin": `<CORSConfiguration><CORSRule><AllowedOrigin>https://?.example.com</AllowedOrigin><AllowedMethod>GET</AllowedMethod></CORSRule></CORSConfiguration>`,
|
||||
"question mark header": `<CORSConfiguration><CORSRule><AllowedOrigin>*</AllowedOrigin><AllowedMethod>GET</AllowedMethod><AllowedHeader>x-amz-?</AllowedHeader></CORSRule></CORSConfiguration>`,
|
||||
"overlong id": `<CORSConfiguration><CORSRule><ID>` + strings.Repeat("a", 256) + `</ID><AllowedOrigin>*</AllowedOrigin><AllowedMethod>GET</AllowedMethod></CORSRule></CORSConfiguration>`,
|
||||
}
|
||||
for name, doc := range cases {
|
||||
@@ -74,14 +77,14 @@ func TestValidateRejections(t *testing.T) {
|
||||
|
||||
func TestMatching(t *testing.T) {
|
||||
c, _ := ParseBucketCorsConfig(strings.NewReader(sampleCORS))
|
||||
rule, ok := c.MatchRule("https://api.example.org", "GET")
|
||||
rule, _, ok := c.MatchRule("https://api.example.org", "GET")
|
||||
if !ok {
|
||||
t.Fatal("expected origin+method to match")
|
||||
}
|
||||
if _, ok := c.MatchRule("http://evil.com", "GET"); ok {
|
||||
if _, _, ok := c.MatchRule("http://evil.com", "GET"); ok {
|
||||
t.Fatal("did not expect match for disallowed origin")
|
||||
}
|
||||
if _, ok := c.MatchRule("http://www.example.com", "DELETE"); ok {
|
||||
if _, _, ok := c.MatchRule("http://www.example.com", "DELETE"); ok {
|
||||
t.Fatal("did not expect match for disallowed method")
|
||||
}
|
||||
allowed, ok := rule.FilterAllowedHeaders([]string{"x-amz-date", "x-amz-content-sha256"})
|
||||
@@ -118,7 +121,7 @@ func TestMatchPreflightFallsThroughToLaterRule(t *testing.T) {
|
||||
t.Fatalf("parse failed: %v", err)
|
||||
}
|
||||
|
||||
rule, allowed, ok := c.MatchPreflight("https://app.example.com", "GET", []string{"x-custom-header"})
|
||||
rule, _, allowed, ok := c.MatchPreflight("https://app.example.com", "GET", []string{"x-custom-header"})
|
||||
if !ok {
|
||||
t.Fatal("expected MatchPreflight to succeed via the later, permissive rule")
|
||||
}
|
||||
@@ -129,3 +132,26 @@ func TestMatchPreflightFallsThroughToLaterRule(t *testing.T) {
|
||||
t.Fatalf("unexpected allowed headers: %v", allowed)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMatchAllowedOriginReturnsFirstMatchingPattern(t *testing.T) {
|
||||
rule := Rule{AllowedOrigins: []string{"https://app.example.com", "https://*", "*"}}
|
||||
|
||||
tests := []struct {
|
||||
origin string
|
||||
want string
|
||||
}{
|
||||
{"https://app.example.com", "https://app.example.com"},
|
||||
{"https://other.example.com", "https://*"},
|
||||
{"http://other.example.com", "*"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
got, ok := rule.matchAllowedOrigin(tt.origin)
|
||||
if !ok {
|
||||
t.Fatalf("expected %q to match", tt.origin)
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Fatalf("origin %q matched %q, want %q", tt.origin, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user