Skip to content

Commit

Permalink
Merge pull request cubefs#507 from wenjia322/cors
Browse files Browse the repository at this point in the history
Feature: support CORS access control
  • Loading branch information
mervinkid authored May 22, 2020
2 parents 1d4d6a7 + 4a1f4fa commit 0db065b
Show file tree
Hide file tree
Showing 12 changed files with 341 additions and 40 deletions.
3 changes: 2 additions & 1 deletion docker/conf/objectnode.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
"action:oss:GetObjectXAttr",
"action:oss:PutObjectXAttr",
"action:oss:DeleteObjectXAttr",
"action:oss:ListObjectXAttrs"
"action:oss:ListObjectXAttrs",
"action:oss:OptionsObject"
],
"disabledActions": [
"action:oss:CreateBucket",
Expand Down
2 changes: 1 addition & 1 deletion objectnode/acl.go
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,7 @@ func storeBucketACL(bytes []byte, vol *Volume) (*AccessControlPolicy, error) {
return nil, err3
}

err4 := vol.store.Put(vol.name, bucketRootPath, OSS_ACL_KEY, bytes)
err4 := vol.store.Put(vol.name, bucketRootPath, XAttrKeyOSSACL, bytes)
if err4 != nil {
return nil, err4
}
Expand Down
1 change: 0 additions & 1 deletion objectnode/acl_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import (
)

const (
OSS_ACL_KEY = "oss:acl"
XMLNS = "http://www.w3.org/2001/XMLSchema-instance"
XMLXSI = "CanonicalUser"
DEF_GRANTEE_TYPE = "CanonicalUser" //
Expand Down
44 changes: 39 additions & 5 deletions objectnode/api_middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"fmt"
"net/http"
"regexp"
"strconv"
"strings"
"time"

Expand Down Expand Up @@ -224,11 +225,44 @@ func (o *ObjectNode) expectMiddleware(next http.Handler) http.Handler {
// request → [pre-handle] → [next handler] → response
func (o *ObjectNode) corsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// write access control allow headers
w.Header()[HeaderNameAccessControlAllowOrigin] = []string{"*"}
w.Header()[HeaderNameAccessControlAllowHeaders] = []string{"*"}
w.Header()[HeaderNameAccessControlAllowMethods] = []string{"*"}
w.Header()[HeaderNameAccessControlMaxAge] = []string{"0"}

var err error
var param = ParseRequestParam(r)
if param.Bucket() == "" {
next.ServeHTTP(w, r)
return
}
var vol *Volume
if vol, err = o.vm.Volume(param.Bucket()); err != nil {
next.ServeHTTP(w, r)
return
}

var setupCORSHeader = func(volume *Volume, writer http.ResponseWriter, request *http.Request) {
origin := request.Header.Get(Origin)
method := request.Header.Get(HeaderNameAccessControlRequestMethod)
headerStr := request.Header.Get(HeaderNameAccessControlRequestHeaders)
if origin == "" || method == "" {
return
}
cors := volume.loadCors()
if cors != nil {
headers := strings.Split(headerStr, ",")
for _, corsRule := range cors.CORSRule {
if corsRule.match(origin, method, headers) {
// write access control allow headers
writer.Header()[HeaderNameAccessControlAllowOrigin] = []string{origin}
writer.Header()[HeaderNameAccessControlMaxAge] = []string{strconv.Itoa(int(corsRule.MaxAgeSeconds))}
writer.Header()[HeaderNameAccessControlAllowMethods] = []string{strings.Join(corsRule.AllowedMethod, ",")}
writer.Header()[HeaderNameAccessControlAllowHeaders] = []string{strings.Join(corsRule.AllowedHeader, ",")}
writer.Header()[HeaderNamrAccessControlExposeHeaders] = []string{strings.Join(corsRule.ExposeHeader, ",")}
return
}
}
}
}
setupCORSHeader(vol, w, r)
next.ServeHTTP(w, r)
return
})
}
18 changes: 12 additions & 6 deletions objectnode/const.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,14 @@ const (
HeaderNameLocation = "Location"

// Headers for CORS validation
HeaderNameAccessControlAllowOrigin = "Access-Control-Allow-Origin"
HeaderNameAccessControlAllowMethods = "Access-Control-Allow-Methods"
HeaderNameAccessControlAllowHeaders = "Access-Control-Allow-Headers"
HeaderNameAccessControlMaxAge = "Access-Control-Max-Age"
Origin = "Origin"
HeaderNameAccessControlRequestMethod = "Access-Control-Request-Method"
HeaderNameAccessControlRequestHeaders = "Access-Control-Request-Headers"
HeaderNameAccessControlAllowOrigin = "Access-Control-Allow-Origin"
HeaderNameAccessControlMaxAge = "Access-Control-Max-Age"
HeaderNameAccessControlAllowMethods = "Access-Control-Allow-Methods"
HeaderNameAccessControlAllowHeaders = "Access-Control-Allow-Headers"
HeaderNamrAccessControlExposeHeaders = "Access-Control-Expose-Headers"

HeaderNameXAmzStartDate = "x-amz-date"
HeaderNameXAmzRequestId = "x-amz-request-id"
Expand Down Expand Up @@ -112,11 +116,13 @@ const (
XAttrKeyOSSETag = "oss:etag"
XAttrKeyOSSTagging = "oss:tagging"
XAttrKeyOSSPolicy = "oss:policy"
XAttrKeyOSSACL = "oss:acl"
XAttrKeyOSSMIME = "oss:mime"
XAttrKeyOSSDISPOSITION = "oss:disposition"
XAttrKeyOSSCORS = "oss:cors"

// Departure
XAttrKeyOSSETagInvalid = "oss:tag"
// Deprecated
XAttrKeyOSSETagDeprecated = "oss:tag"
)

const (
Expand Down
82 changes: 82 additions & 0 deletions objectnode/cors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package objectnode

// https://docs.aws.amazon.com/AmazonS3/latest/dev/cors.html

import (
"encoding/xml"

"github.com/chubaofs/chubaofs/util/errors"
)

var methodsRequest = []string{"GET", "PUT", "HEAD", "POST", "DELETE", "*"}

type CORSConfiguration struct {
XMLName xml.Name `xml:"CORSConfiguration" json:"xml_name"`
CORSRule []*CORSRule `xml:"CORSRule" json:"cors_rule"`
}

type CORSRule struct {
AllowedHeader []string `xml:"AllowedHeader" json:"allowed_header"`
AllowedMethod []string `xml:"AllowedMethod" json:"allowed_method"`
AllowedOrigin []string `xml:"AllowedOrigin" json:"allowed_origin"`
ExposeHeader []string `xml:"ExposeHeader" json:"expose_header"`
MaxAgeSeconds uint16 `xml:"MaxAgeSeconds" json:"max_age_seconds"`
}

func (rule *CORSRule) match(origin, method string, headers []string) bool {
// todo if "*" are used in some text
if !contains(rule.AllowedOrigin, "*") && !contains(rule.AllowedOrigin, origin) {
return false
}
if !contains(rule.AllowedMethod, "*") && !contains(rule.AllowedMethod, method) {
return false
}
if contains(rule.AllowedHeader, "*") {
return true
}
for _, header := range headers {
if !contains(rule.AllowedHeader, header) {
return false
}
}
return true
}

func (corsConfig *CORSConfiguration) validate() bool {
if len(corsConfig.CORSRule) > 100 {
return false
}
for _, rule := range corsConfig.CORSRule {
for _, method := range rule.AllowedMethod {
if !contains(methodsRequest, method) {
return false
}
}
}
return true
}

func parseCorsConfig(bytes []byte) (corsConfig *CORSConfiguration, err error) {
corsConfig = &CORSConfiguration{}
if err = xml.Unmarshal(bytes, corsConfig); err != nil {
return
}
if ok := corsConfig.validate(); !ok {
return nil, errors.New("invalid cors configuration")
}
return
}

func storeBucketCors(bytes []byte, vol *Volume) (err error) {
if err = vol.store.Put(vol.name, bucketRootPath, XAttrKeyOSSCORS, bytes); err != nil {
return
}
return nil
}

func deleteBucketCors(vol *Volume) (err error) {
if err = vol.store.Delete(vol.name, bucketRootPath, XAttrKeyOSSCORS); err != nil {
return err
}
return nil
}
125 changes: 125 additions & 0 deletions objectnode/cors_handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package objectnode

// https://docs.aws.amazon.com/zh_cn/AmazonS3/latest/dev/EnableCorsUsingREST.html

import (
"encoding/json"
"encoding/xml"
"io"
"io/ioutil"
"net/http"

"github.com/chubaofs/chubaofs/util/log"
)

// https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetBucketCors.html
func (o *ObjectNode) getBucketCorsHandler(w http.ResponseWriter, r *http.Request) {

var err error
var param = ParseRequestParam(r)
if param.Bucket() == "" {
_ = NoSuchBucket.ServeResponse(w, r)
return
}

var vol *Volume
if vol, err = o.vm.Volume(param.Bucket()); err != nil {
_ = NoSuchBucket.ServeResponse(w, r)
return
}

var output = CORSConfiguration{}

cors := vol.loadCors()
if cors != nil {
output.CORSRule = cors.CORSRule
}
var corsData []byte
if corsData, err = xml.Marshal(output); err != nil {
_ = InternalErrorCode(err).ServeResponse(w, r)
return
}

_, _ = w.Write(corsData)
return
}

// https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutBucketCors.html
func (o *ObjectNode) putBucketCorsHandler(w http.ResponseWriter, r *http.Request) {
log.LogInfof("Put bucket cors")

var err error
var param = ParseRequestParam(r)
if param.Bucket() == "" {
_ = NoSuchBucket.ServeResponse(w, r)
return
}
var vol *Volume
if vol, err = o.vm.Volume(param.Bucket()); err != nil {
_ = NoSuchBucket.ServeResponse(w, r)
return
}

var bytes []byte
if bytes, err = ioutil.ReadAll(r.Body); err != nil && err != io.EOF {
_ = InternalErrorCode(err).ServeResponse(w, r)
return
}

var corsConfig *CORSConfiguration
if corsConfig, err = parseCorsConfig(bytes); err != nil {
_ = InvalidArgument.ServeResponse(w, r)
return
}
if corsConfig == nil {
_ = InvalidArgument.ServeResponse(w, r)
return
}

var newBytes []byte
if newBytes, err = json.Marshal(corsConfig); err != nil {
_ = InternalErrorCode(err).ServeResponse(w, r)
return
}
if err = storeBucketCors(newBytes, vol); err != nil {
_ = InternalErrorCode(err).ServeResponse(w, r)
return
}
vol.storeCors(corsConfig)

return
}

// https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteBucketCors.html
func (o *ObjectNode) deleteBucketCorsHandler(w http.ResponseWriter, r *http.Request) {
log.LogInfof("Delete bucket cors")

var err error
var param = ParseRequestParam(r)
if param.Bucket() == "" {
_ = NoSuchBucket.ServeResponse(w, r)
return
}
var vol *Volume
if vol, err = o.vm.Volume(param.Bucket()); err != nil {
_ = NoSuchBucket.ServeResponse(w, r)
return
}

if err = deleteBucketCors(vol); err != nil {
_ = InternalErrorCode(err).ServeResponse(w, r)
return
}
vol.storeCors(nil)

w.WriteHeader(http.StatusNoContent)
return
}

// Option object
// Reference: https://docs.aws.amazon.com/AmazonS3/latest/API/RESTOPTIONSobject.html
func (o *ObjectNode) optionsObjectHandler(w http.ResponseWriter, r *http.Request) {
log.LogInfof("optionsObjectHandler: OPTIONS object, requestID(%v) remote(%v)", GetRequestID(r), r.RemoteAddr)
// Already done in methods 'corsMiddleware'.
return
}
15 changes: 11 additions & 4 deletions objectnode/fs_store_xattr.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ func (s *xattrStore) Put(vol, path, key string, data []byte) (err error) {
}
err = v.SetXAttr(path, key, data)
if err != nil {
log.LogErrorf("policy: %v, %v", key, data)
log.LogErrorf("put xattr failed: vol[%v], key[%v], data[%v]", vol, key, data)
}
return
}
Expand Down Expand Up @@ -92,9 +92,16 @@ func (s *xattrStore) Get(vol, path, key string) (val []byte, err error) {
return
}

func (s *xattrStore) Delete(vol, obj, key string) (err error) {

return
func (s *xattrStore) Delete(vol, path, key string) (err error) {
var v *Volume
if v, err = s.vm.Volume(vol); err != nil {
return
}
if err = v.DeleteXAttr(path, key); err != nil {
log.LogErrorf("delete xattr failed: vol[%v], key[%v]", vol, key)
return
}
return nil
}

func (s *xattrStore) List(vol, obj string) (data [][]byte, err error) {
Expand Down
Loading

0 comments on commit 0db065b

Please sign in to comment.