diff --git a/cmd/api-router.go b/cmd/api-router.go
index 188dd854f..99733f8b2 100644
--- a/cmd/api-router.go
+++ b/cmd/api-router.go
@@ -111,11 +111,6 @@ var rejectedBucketAPIs = []rejectedAPI{
methods: []string{http.MethodGet, http.MethodPut, http.MethodDelete},
queries: []string{"inventory", ""},
},
- {
- api: "cors",
- methods: []string{http.MethodPut, http.MethodDelete},
- queries: []string{"cors", ""},
- },
{
api: "metrics",
methods: []string{http.MethodGet, http.MethodPut, http.MethodDelete},
diff --git a/cmd/bucket-cors-handlers.go b/cmd/bucket-cors-handlers.go
new file mode 100644
index 000000000..d38e5f3be
--- /dev/null
+++ b/cmd/bucket-cors-handlers.go
@@ -0,0 +1,197 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "bytes"
+ "encoding/base64"
+ "encoding/xml"
+ "errors"
+ "io"
+ "net/http"
+
+ humanize "github.com/dustin/go-humanize"
+ "github.com/minio/madmin-go/v3"
+ "github.com/minio/minio/internal/bucket/cors"
+ "github.com/minio/minio/internal/logger"
+ "github.com/minio/mux"
+ "github.com/minio/pkg/v3/policy"
+)
+
+// maxBucketCorsSize is the maximum allowed size of a CORS configuration document.
+const maxBucketCorsSize = 64 * humanize.KiByte
+
+// PutBucketCorsHandler - PUT bucket cors.
+func (api objectAPIHandlers) PutBucketCorsHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := newContext(r, w, "PutBucketCors")
+
+ defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
+
+ objAPI := api.ObjectAPI()
+ if objAPI == nil {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
+ return
+ }
+
+ vars := mux.Vars(r)
+ bucket := vars["bucket"]
+
+ if s3Error := checkRequestAuthType(ctx, r, policy.PutBucketCorsAction, bucket, ""); s3Error != ErrNone {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
+ return
+ }
+
+ if _, err := objAPI.GetBucketInfo(ctx, bucket, BucketOptions{}); err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ if r.ContentLength <= 0 {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMissingContentLength), r.URL)
+ return
+ }
+ if r.ContentLength > maxBucketCorsSize {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrEntityTooLarge), r.URL)
+ return
+ }
+
+ corsBytes, err := io.ReadAll(io.LimitReader(r.Body, r.ContentLength))
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ corsCfg, err := cors.ParseBucketCorsConfig(bytes.NewReader(corsBytes))
+ if err != nil {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMalformedXML), r.URL)
+ return
+ }
+ if err := corsCfg.Validate(); err != nil {
+ writeErrorResponse(ctx, w, APIError{
+ Code: "MalformedXML",
+ HTTPStatusCode: http.StatusBadRequest,
+ Description: err.Error(),
+ }, r.URL)
+ return
+ }
+
+ updatedAt, err := globalBucketMetadataSys.Update(ctx, bucket, bucketCorsConfig, corsBytes)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ // Call site replication hook.
+ //
+ // We encode the xml bytes as base64 to ensure there are no encoding
+ // errors.
+ cfgStr := base64.StdEncoding.EncodeToString(corsBytes)
+ replLogIf(ctx, globalSiteReplicationSys.BucketMetaHook(ctx, madmin.SRBucketMeta{
+ Type: madmin.SRBucketMetaTypeCorsConfig,
+ Bucket: bucket,
+ Cors: &cfgStr,
+ UpdatedAt: updatedAt,
+ }))
+
+ writeSuccessResponseHeadersOnly(w)
+}
+
+// GetBucketCorsHandler - GET bucket cors.
+func (api objectAPIHandlers) GetBucketCorsHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := newContext(r, w, "GetBucketCors")
+
+ defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
+
+ objAPI := api.ObjectAPI()
+ if objAPI == nil {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
+ return
+ }
+
+ vars := mux.Vars(r)
+ bucket := vars["bucket"]
+
+ if s3Error := checkRequestAuthType(ctx, r, policy.GetBucketCorsAction, bucket, ""); s3Error != ErrNone {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
+ return
+ }
+
+ if _, err := objAPI.GetBucketInfo(ctx, bucket, BucketOptions{}); err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ config, _, err := globalBucketMetadataSys.GetCorsConfig(bucket)
+ if err != nil {
+ if errors.Is(err, errConfigNotFound) {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrNoSuchCORSConfiguration), r.URL)
+ return
+ }
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ configData, err := xml.Marshal(config)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ writeSuccessResponseXML(w, configData)
+}
+
+// DeleteBucketCorsHandler - DELETE bucket cors.
+func (api objectAPIHandlers) DeleteBucketCorsHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := newContext(r, w, "DeleteBucketCors")
+
+ defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
+
+ objAPI := api.ObjectAPI()
+ if objAPI == nil {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
+ return
+ }
+
+ vars := mux.Vars(r)
+ bucket := vars["bucket"]
+
+ if s3Error := checkRequestAuthType(ctx, r, policy.DeleteBucketCorsAction, bucket, ""); s3Error != ErrNone {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
+ return
+ }
+
+ if _, err := objAPI.GetBucketInfo(ctx, bucket, BucketOptions{}); err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ updatedAt, err := globalBucketMetadataSys.Delete(ctx, bucket, bucketCorsConfig)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ replLogIf(ctx, globalSiteReplicationSys.BucketMetaHook(ctx, madmin.SRBucketMeta{
+ Type: madmin.SRBucketMetaTypeCorsConfig,
+ Bucket: bucket,
+ Cors: nil,
+ UpdatedAt: updatedAt,
+ }))
+
+ writeSuccessNoContent(w)
+}
diff --git a/cmd/bucket-cors-handlers_test.go b/cmd/bucket-cors-handlers_test.go
new file mode 100644
index 000000000..22932fdaa
--- /dev/null
+++ b/cmd/bucket-cors-handlers_test.go
@@ -0,0 +1,100 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "bytes"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "github.com/minio/minio/internal/auth"
+)
+
+const testCORSDoc = `http://example.comGETPUTETag3000`
+
+func TestBucketCorsHandlers(t *testing.T) {
+ ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{t: t, objAPITest: testBucketCorsHandlers, endpoints: []string{"PutBucketCors", "GetBucketCors", "DeleteBucketCors"}})
+}
+
+func testBucketCorsHandlers(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
+ creds auth.Credentials, t *testing.T,
+) {
+ // PUT
+ req, err := newTestSignedRequestV4(http.MethodPut, getBucketCorsURL("", bucketName),
+ int64(len(testCORSDoc)), bytes.NewReader([]byte(testCORSDoc)), creds.AccessKey, creds.SecretKey, nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ rec := httptest.NewRecorder()
+ apiRouter.ServeHTTP(rec, req)
+ if rec.Code != http.StatusOK {
+ t.Fatalf("PUT cors: expected 200, got %d: %s", rec.Code, rec.Body.String())
+ }
+
+ // GET returns what we stored
+ req, err = newTestSignedRequestV4(http.MethodGet, getBucketCorsURL("", bucketName),
+ 0, nil, creds.AccessKey, creds.SecretKey, nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ rec = httptest.NewRecorder()
+ apiRouter.ServeHTTP(rec, req)
+ if rec.Code != http.StatusOK {
+ t.Fatalf("GET cors: expected 200, got %d: %s", rec.Code, rec.Body.String())
+ }
+ if !bytes.Contains(rec.Body.Bytes(), []byte("http://example.com")) {
+ t.Fatalf("GET cors: body missing origin: %s", rec.Body.String())
+ }
+
+ // DELETE
+ req, err = newTestSignedRequestV4(http.MethodDelete, getBucketCorsURL("", bucketName),
+ 0, nil, creds.AccessKey, creds.SecretKey, nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ rec = httptest.NewRecorder()
+ apiRouter.ServeHTTP(rec, req)
+ if rec.Code != http.StatusNoContent {
+ t.Fatalf("DELETE cors: expected 204, got %d", rec.Code)
+ }
+
+ // GET after delete → 404 NoSuchCORSConfiguration
+ req, err = newTestSignedRequestV4(http.MethodGet, getBucketCorsURL("", bucketName),
+ 0, nil, creds.AccessKey, creds.SecretKey, nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ rec = httptest.NewRecorder()
+ apiRouter.ServeHTTP(rec, req)
+ if rec.Code != http.StatusNotFound {
+ t.Fatalf("GET cors after delete: expected 404, got %d", rec.Code)
+ }
+
+ // Malformed XML → 400
+ req, err = newTestSignedRequestV4(http.MethodPut, getBucketCorsURL("", bucketName),
+ int64(len("")), bytes.NewReader([]byte("")), creds.AccessKey, creds.SecretKey, nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ rec = httptest.NewRecorder()
+ apiRouter.ServeHTTP(rec, req)
+ if rec.Code != http.StatusBadRequest {
+ t.Fatalf("PUT malformed cors: expected 400, got %d", rec.Code)
+ }
+}
diff --git a/cmd/dummy-handlers.go b/cmd/dummy-handlers.go
index 685b79256..1781c228f 100644
--- a/cmd/dummy-handlers.go
+++ b/cmd/dummy-handlers.go
@@ -165,93 +165,3 @@ func (api objectAPIHandlers) GetBucketLoggingHandler(w http.ResponseWriter, r *h
func (api objectAPIHandlers) DeleteBucketWebsiteHandler(w http.ResponseWriter, r *http.Request) {
writeSuccessResponseHeadersOnly(w)
}
-
-// GetBucketCorsHandler - GET bucket cors, a dummy api
-func (api objectAPIHandlers) GetBucketCorsHandler(w http.ResponseWriter, r *http.Request) {
- ctx := newContext(r, w, "GetBucketCors")
-
- defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
-
- vars := mux.Vars(r)
- bucket := vars["bucket"]
-
- objAPI := api.ObjectAPI()
- if objAPI == nil {
- writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
- return
- }
-
- if s3Error := checkRequestAuthType(ctx, r, policy.GetBucketCorsAction, bucket, ""); s3Error != ErrNone {
- writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
- return
- }
-
- // Validate if bucket exists, before proceeding further...
- _, err := objAPI.GetBucketInfo(ctx, bucket, BucketOptions{})
- if err != nil {
- writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
- return
- }
-
- writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrNoSuchCORSConfiguration), r.URL)
-}
-
-// PutBucketCorsHandler - PUT bucket cors, a dummy api
-func (api objectAPIHandlers) PutBucketCorsHandler(w http.ResponseWriter, r *http.Request) {
- ctx := newContext(r, w, "PutBucketCors")
-
- defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
-
- vars := mux.Vars(r)
- bucket := vars["bucket"]
-
- objAPI := api.ObjectAPI()
- if objAPI == nil {
- writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
- return
- }
-
- if s3Error := checkRequestAuthType(ctx, r, policy.PutBucketCorsAction, bucket, ""); s3Error != ErrNone {
- writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
- return
- }
-
- // Validate if bucket exists, before proceeding further...
- _, err := objAPI.GetBucketInfo(ctx, bucket, BucketOptions{})
- if err != nil {
- writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
- return
- }
-
- writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
-}
-
-// DeleteBucketCorsHandler - DELETE bucket cors, a dummy api
-func (api objectAPIHandlers) DeleteBucketCorsHandler(w http.ResponseWriter, r *http.Request) {
- ctx := newContext(r, w, "DeleteBucketCors")
-
- defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
-
- vars := mux.Vars(r)
- bucket := vars["bucket"]
-
- objAPI := api.ObjectAPI()
- if objAPI == nil {
- writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
- return
- }
-
- if s3Error := checkRequestAuthType(ctx, r, policy.DeleteBucketCorsAction, bucket, ""); s3Error != ErrNone {
- writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
- return
- }
-
- // Validate if bucket exists, before proceeding further...
- _, err := objAPI.GetBucketInfo(ctx, bucket, BucketOptions{})
- if err != nil {
- writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
- return
- }
-
- writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
-}
diff --git a/cmd/test-utils_test.go b/cmd/test-utils_test.go
index 0f903625c..6d0e7bf64 100644
--- a/cmd/test-utils_test.go
+++ b/cmd/test-utils_test.go
@@ -1373,6 +1373,11 @@ func getBucketLifecycleURL(endPoint, bucketName string) (ret string) {
return makeTestTargetURL(endPoint, bucketName, "", queryValue)
}
+// return URL for set/get/delete cors of the bucket.
+func getBucketCorsURL(endPoint, bucketName string) string {
+ return makeTestTargetURL(endPoint, bucketName, "", url.Values{"cors": []string{""}})
+}
+
// return URL for listing objects in the bucket with V1 legacy API.
func getListObjectsV1URL(endPoint, bucketName, prefix, maxKeys, encodingType string) string {
queryValue := url.Values{}
@@ -2052,6 +2057,15 @@ func registerBucketLevelFunc(bucket *mux.Router, api objectAPIHandlers, apiFunct
case "ListenNotification":
// Register ListenNotification Handler.
bucket.Methods(http.MethodGet).HandlerFunc(api.ListenNotificationHandler).Queries("events", "{events:.*}")
+ case "PutBucketCors":
+ // Register PutBucketCors handler.
+ bucket.Methods(http.MethodPut).HandlerFunc(api.PutBucketCorsHandler).Queries("cors", "")
+ case "GetBucketCors":
+ // Register GetBucketCors handler.
+ bucket.Methods(http.MethodGet).HandlerFunc(api.GetBucketCorsHandler).Queries("cors", "")
+ case "DeleteBucketCors":
+ // Register DeleteBucketCors handler.
+ bucket.Methods(http.MethodDelete).HandlerFunc(api.DeleteBucketCorsHandler).Queries("cors", "")
}
}
}