[##] Add support virtual-hosted-style
Signed-off-by: Roman Loginov <r.loginov@yadro.com>
This commit is contained in:
parent
c34680d157
commit
82eee8a246
13 changed files with 320 additions and 201 deletions
125
api/middleware/address_style.go
Normal file
125
api/middleware/address_style.go
Normal file
|
@ -0,0 +1,125 @@
|
||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const wildcardPlaceholder = "<wildcard>"
|
||||||
|
|
||||||
|
type VHSSettings interface {
|
||||||
|
Domains() []string
|
||||||
|
GlobalVHS() bool
|
||||||
|
VHSNamespacesEnabled() map[string]bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func PrepareAddressStyle(settings VHSSettings) Func {
|
||||||
|
return func(h http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
reqInfo := GetReqInfo(ctx)
|
||||||
|
|
||||||
|
if isVHSAddress(settings.GlobalVHS(), settings.VHSNamespacesEnabled(), reqInfo.Namespace) {
|
||||||
|
prepareVHSAddress(reqInfo, r, settings)
|
||||||
|
} else {
|
||||||
|
preparePathStyleAddress(reqInfo, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
h.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func isVHSAddress(enabledFlag bool, vhsNamespaces map[string]bool, namespace string) bool {
|
||||||
|
result := enabledFlag
|
||||||
|
|
||||||
|
if v, ok := vhsNamespaces[namespace]; ok {
|
||||||
|
result = v
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func prepareVHSAddress(reqInfo *ReqInfo, r *http.Request, settings VHSSettings) {
|
||||||
|
reqInfo.RequestVHSEnabled = true
|
||||||
|
bktName, match := checkDomain(r.Host, settings.Domains())
|
||||||
|
if match {
|
||||||
|
if bktName == "" {
|
||||||
|
reqInfo.RequestType = noneType
|
||||||
|
} else {
|
||||||
|
if objName := strings.TrimPrefix(r.URL.Path, "/"); objName != "" {
|
||||||
|
reqInfo.RequestType = objectType
|
||||||
|
reqInfo.ObjectName = objName
|
||||||
|
reqInfo.BucketName = bktName
|
||||||
|
} else {
|
||||||
|
reqInfo.RequestType = bucketType
|
||||||
|
reqInfo.BucketName = bktName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
parts := strings.Split(r.Host, ".")
|
||||||
|
reqInfo.BucketName = parts[0]
|
||||||
|
|
||||||
|
if objName := strings.TrimPrefix(r.URL.Path, "/"); objName != "" {
|
||||||
|
reqInfo.RequestType = objectType
|
||||||
|
reqInfo.ObjectName = objName
|
||||||
|
} else {
|
||||||
|
reqInfo.RequestType = bucketType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func preparePathStyleAddress(reqInfo *ReqInfo, r *http.Request) {
|
||||||
|
bktObj := strings.TrimPrefix(r.URL.Path, "/")
|
||||||
|
if bktObj == "" {
|
||||||
|
reqInfo.RequestType = noneType
|
||||||
|
} else if ind := strings.IndexByte(bktObj, '/'); ind != -1 && bktObj[ind+1:] != "" {
|
||||||
|
reqInfo.RequestType = objectType
|
||||||
|
reqInfo.BucketName = bktObj[:ind]
|
||||||
|
reqInfo.ObjectName = bktObj[ind+1:]
|
||||||
|
} else {
|
||||||
|
reqInfo.RequestType = bucketType
|
||||||
|
reqInfo.BucketName = strings.TrimSuffix(bktObj, "/")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkDomain(requestURL string, domains []string) (bktName string, match bool) {
|
||||||
|
host := strings.Split(requestURL, ".")
|
||||||
|
for _, pattern := range domains {
|
||||||
|
p := strings.Split(pattern, ".")
|
||||||
|
bktName, match = compareMatch(host, p)
|
||||||
|
if match {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func compareMatch(host, pattern []string) (bktName string, match bool) {
|
||||||
|
if len(host) < len(pattern) {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
var patternIndex, hostIndex int
|
||||||
|
for i := 0; i < len(pattern); i++ {
|
||||||
|
patternIndex = len(pattern) - 1 - i
|
||||||
|
hostIndex = len(host) - 1 - i
|
||||||
|
|
||||||
|
if pattern[patternIndex] == wildcardPlaceholder {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if host[hostIndex] != pattern[patternIndex] {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch hostIndex {
|
||||||
|
case 0:
|
||||||
|
return "", true
|
||||||
|
case 1:
|
||||||
|
return host[0], true
|
||||||
|
default:
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
}
|
|
@ -73,7 +73,6 @@ type PolicyConfig struct {
|
||||||
Storage engine.ChainRouter
|
Storage engine.ChainRouter
|
||||||
FrostfsID FrostFSIDInformer
|
FrostfsID FrostFSIDInformer
|
||||||
Settings PolicySettings
|
Settings PolicySettings
|
||||||
Domains []string
|
|
||||||
Log *zap.Logger
|
Log *zap.Logger
|
||||||
BucketResolver BucketResolveFunc
|
BucketResolver BucketResolveFunc
|
||||||
Decoder XMLDecoder
|
Decoder XMLDecoder
|
||||||
|
@ -99,21 +98,21 @@ func PolicyCheck(cfg PolicyConfig) Func {
|
||||||
}
|
}
|
||||||
|
|
||||||
func policyCheck(r *http.Request, cfg PolicyConfig) error {
|
func policyCheck(r *http.Request, cfg PolicyConfig) error {
|
||||||
reqType, bktName, objName := getBucketObject(r, cfg.Domains)
|
reqInfo := GetReqInfo(r.Context())
|
||||||
req, userKey, userGroups, err := getPolicyRequest(r, cfg, reqType, bktName, objName)
|
|
||||||
|
req, userKey, userGroups, err := getPolicyRequest(r, cfg, reqInfo.RequestType, reqInfo.BucketName, reqInfo.ObjectName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var bktInfo *data.BucketInfo
|
var bktInfo *data.BucketInfo
|
||||||
if reqType != noneType && !strings.HasSuffix(req.Operation(), CreateBucketOperation) {
|
if reqInfo.RequestType != noneType && !strings.HasSuffix(req.Operation(), CreateBucketOperation) {
|
||||||
bktInfo, err = cfg.BucketResolver(r.Context(), bktName)
|
bktInfo, err = cfg.BucketResolver(r.Context(), reqInfo.BucketName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
reqInfo := GetReqInfo(r.Context())
|
|
||||||
target := engine.NewRequestTargetWithNamespace(reqInfo.Namespace)
|
target := engine.NewRequestTargetWithNamespace(reqInfo.Namespace)
|
||||||
if bktInfo != nil {
|
if bktInfo != nil {
|
||||||
cnrTarget := engine.ContainerTarget(bktInfo.CID.EncodeToString())
|
cnrTarget := engine.ContainerTarget(bktInfo.CID.EncodeToString())
|
||||||
|
@ -208,33 +207,6 @@ const (
|
||||||
objectType
|
objectType
|
||||||
)
|
)
|
||||||
|
|
||||||
func getBucketObject(r *http.Request, domains []string) (reqType ReqType, bktName string, objName string) {
|
|
||||||
for _, domain := range domains {
|
|
||||||
ind := strings.Index(r.Host, "."+domain)
|
|
||||||
if ind == -1 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
bkt := r.Host[:ind]
|
|
||||||
if obj := strings.TrimPrefix(r.URL.Path, "/"); obj != "" {
|
|
||||||
return objectType, bkt, obj
|
|
||||||
}
|
|
||||||
|
|
||||||
return bucketType, bkt, ""
|
|
||||||
}
|
|
||||||
|
|
||||||
bktObj := strings.TrimPrefix(r.URL.Path, "/")
|
|
||||||
if bktObj == "" {
|
|
||||||
return noneType, "", ""
|
|
||||||
}
|
|
||||||
|
|
||||||
if ind := strings.IndexByte(bktObj, '/'); ind != -1 && bktObj[ind+1:] != "" {
|
|
||||||
return objectType, bktObj[:ind], bktObj[ind+1:]
|
|
||||||
}
|
|
||||||
|
|
||||||
return bucketType, strings.TrimSuffix(bktObj, "/"), ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func determineOperation(r *http.Request, reqType ReqType) (operation string) {
|
func determineOperation(r *http.Request, reqType ReqType) (operation string) {
|
||||||
switch reqType {
|
switch reqType {
|
||||||
case objectType:
|
case objectType:
|
||||||
|
|
|
@ -8,79 +8,6 @@ import (
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestReqTypeDetermination(t *testing.T) {
|
|
||||||
bkt, obj, domain := "test-bucket", "test-object", "domain"
|
|
||||||
|
|
||||||
for _, tc := range []struct {
|
|
||||||
name string
|
|
||||||
target string
|
|
||||||
host string
|
|
||||||
domains []string
|
|
||||||
expectedType ReqType
|
|
||||||
expectedBktName string
|
|
||||||
expectedObjName string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "bucket request, path-style",
|
|
||||||
target: "/" + bkt,
|
|
||||||
expectedType: bucketType,
|
|
||||||
expectedBktName: bkt,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "bucket request with slash, path-style",
|
|
||||||
target: "/" + bkt + "/",
|
|
||||||
expectedType: bucketType,
|
|
||||||
expectedBktName: bkt,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "object request, path-style",
|
|
||||||
target: "/" + bkt + "/" + obj,
|
|
||||||
expectedType: objectType,
|
|
||||||
expectedBktName: bkt,
|
|
||||||
expectedObjName: obj,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "object request with slash, path-style",
|
|
||||||
target: "/" + bkt + "/" + obj + "/",
|
|
||||||
expectedType: objectType,
|
|
||||||
expectedBktName: bkt,
|
|
||||||
expectedObjName: obj + "/",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "none type request",
|
|
||||||
target: "/",
|
|
||||||
expectedType: noneType,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "bucket request, virtual-hosted style",
|
|
||||||
target: "/",
|
|
||||||
host: bkt + "." + domain,
|
|
||||||
domains: []string{"some-domain", domain},
|
|
||||||
expectedType: bucketType,
|
|
||||||
expectedBktName: bkt,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "object request, virtual-hosted style",
|
|
||||||
target: "/" + obj,
|
|
||||||
host: bkt + "." + domain,
|
|
||||||
domains: []string{"some-domain", domain},
|
|
||||||
expectedType: objectType,
|
|
||||||
expectedBktName: bkt,
|
|
||||||
expectedObjName: obj,
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
r := httptest.NewRequest(http.MethodPut, tc.target, nil)
|
|
||||||
r.Host = tc.host
|
|
||||||
|
|
||||||
reqType, bktName, objName := getBucketObject(r, tc.domains)
|
|
||||||
require.Equal(t, tc.expectedType, reqType)
|
|
||||||
require.Equal(t, tc.expectedBktName, bktName)
|
|
||||||
require.Equal(t, tc.expectedObjName, objName)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDetermineBucketOperation(t *testing.T) {
|
func TestDetermineBucketOperation(t *testing.T) {
|
||||||
const defaultValue = "value"
|
const defaultValue = "value"
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,6 @@ import (
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data"
|
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs"
|
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs"
|
||||||
treepool "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/pool/tree"
|
treepool "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/pool/tree"
|
||||||
"github.com/go-chi/chi/v5"
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"google.golang.org/grpc/metadata"
|
"google.golang.org/grpc/metadata"
|
||||||
|
@ -41,6 +40,8 @@ type (
|
||||||
Namespace string
|
Namespace string
|
||||||
User string // User owner id
|
User string // User owner id
|
||||||
Tagging *data.Tagging
|
Tagging *data.Tagging
|
||||||
|
RequestVHSEnabled bool
|
||||||
|
RequestType ReqType
|
||||||
}
|
}
|
||||||
|
|
||||||
// ObjectRequest represents object request data.
|
// ObjectRequest represents object request data.
|
||||||
|
@ -197,57 +198,6 @@ func Request(log *zap.Logger, settings RequestSettings) Func {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddBucketName adds bucket name to ReqInfo from context.
|
|
||||||
func AddBucketName(l *zap.Logger) Func {
|
|
||||||
return func(h http.Handler) http.Handler {
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
ctx := r.Context()
|
|
||||||
|
|
||||||
reqInfo := GetReqInfo(ctx)
|
|
||||||
reqInfo.BucketName = chi.URLParam(r, BucketURLPrm)
|
|
||||||
|
|
||||||
if reqInfo.BucketName != "" {
|
|
||||||
reqLogger := reqLogOrDefault(ctx, l)
|
|
||||||
r = r.WithContext(SetReqLogger(ctx, reqLogger.With(zap.String("bucket", reqInfo.BucketName))))
|
|
||||||
}
|
|
||||||
|
|
||||||
h.ServeHTTP(w, r)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddObjectName adds objects name to ReqInfo from context.
|
|
||||||
func AddObjectName(l *zap.Logger) Func {
|
|
||||||
return func(h http.Handler) http.Handler {
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
ctx := r.Context()
|
|
||||||
reqInfo := GetReqInfo(ctx)
|
|
||||||
reqLogger := reqLogOrDefault(ctx, l)
|
|
||||||
|
|
||||||
rctx := chi.RouteContext(ctx)
|
|
||||||
// trim leading slash (always present)
|
|
||||||
reqInfo.ObjectName = rctx.RoutePath[1:]
|
|
||||||
|
|
||||||
if r.URL.RawPath != "" {
|
|
||||||
// we have to do this because of
|
|
||||||
// https://github.com/go-chi/chi/issues/641
|
|
||||||
// https://github.com/go-chi/chi/issues/642
|
|
||||||
if obj, err := url.PathUnescape(reqInfo.ObjectName); err != nil {
|
|
||||||
reqLogger.Warn(logs.FailedToUnescapeObjectName, zap.Error(err))
|
|
||||||
} else {
|
|
||||||
reqInfo.ObjectName = obj
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if reqInfo.ObjectName != "" {
|
|
||||||
r = r.WithContext(SetReqLogger(ctx, reqLogger.With(zap.String("object", reqInfo.ObjectName))))
|
|
||||||
}
|
|
||||||
|
|
||||||
h.ServeHTTP(w, r)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// getSourceIP retrieves the IP from the X-Forwarded-For, X-Real-IP and RFC7239
|
// getSourceIP retrieves the IP from the X-Forwarded-For, X-Real-IP and RFC7239
|
||||||
// Forwarded headers (in that order), falls back to r.RemoteAddr when everything
|
// Forwarded headers (in that order), falls back to r.RemoteAddr when everything
|
||||||
// else fails.
|
// else fails.
|
||||||
|
|
|
@ -97,6 +97,7 @@ type Settings interface {
|
||||||
s3middleware.RequestSettings
|
s3middleware.RequestSettings
|
||||||
s3middleware.PolicySettings
|
s3middleware.PolicySettings
|
||||||
s3middleware.MetricsSettings
|
s3middleware.MetricsSettings
|
||||||
|
s3middleware.VHSSettings
|
||||||
}
|
}
|
||||||
|
|
||||||
type FrostFSID interface {
|
type FrostFSID interface {
|
||||||
|
@ -113,9 +114,6 @@ type Config struct {
|
||||||
|
|
||||||
MiddlewareSettings Settings
|
MiddlewareSettings Settings
|
||||||
|
|
||||||
// Domains optional. If empty no virtual hosted domains will be attached.
|
|
||||||
Domains []string
|
|
||||||
|
|
||||||
FrostfsID FrostFSID
|
FrostfsID FrostFSID
|
||||||
|
|
||||||
FrostFSIDValidation bool
|
FrostFSIDValidation bool
|
||||||
|
@ -142,11 +140,11 @@ func NewRouter(cfg Config) *chi.Mux {
|
||||||
api.Use(s3middleware.FrostfsIDValidation(cfg.FrostfsID, cfg.Log))
|
api.Use(s3middleware.FrostfsIDValidation(cfg.FrostfsID, cfg.Log))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
api.Use(s3middleware.PrepareAddressStyle(cfg.MiddlewareSettings))
|
||||||
api.Use(s3middleware.PolicyCheck(s3middleware.PolicyConfig{
|
api.Use(s3middleware.PolicyCheck(s3middleware.PolicyConfig{
|
||||||
Storage: cfg.PolicyChecker,
|
Storage: cfg.PolicyChecker,
|
||||||
FrostfsID: cfg.FrostfsID,
|
FrostfsID: cfg.FrostfsID,
|
||||||
Settings: cfg.MiddlewareSettings,
|
Settings: cfg.MiddlewareSettings,
|
||||||
Domains: cfg.Domains,
|
|
||||||
Log: cfg.Log,
|
Log: cfg.Log,
|
||||||
BucketResolver: cfg.Handler.ResolveBucket,
|
BucketResolver: cfg.Handler.ResolveBucket,
|
||||||
Decoder: cfg.XMLDecoder,
|
Decoder: cfg.XMLDecoder,
|
||||||
|
@ -154,22 +152,42 @@ func NewRouter(cfg Config) *chi.Mux {
|
||||||
}))
|
}))
|
||||||
|
|
||||||
defaultRouter := chi.NewRouter()
|
defaultRouter := chi.NewRouter()
|
||||||
defaultRouter.Mount(fmt.Sprintf("/{%s}", s3middleware.BucketURLPrm), bucketRouter(cfg.Handler, cfg.Log))
|
defaultRouter.Mount(fmt.Sprintf("/{%s}", s3middleware.BucketURLPrm), bucketRouter(cfg.Handler))
|
||||||
defaultRouter.Get("/", named("ListBuckets", cfg.Handler.ListBucketsHandler))
|
defaultRouter.Get("/", named(s3middleware.ListBucketsOperation, cfg.Handler.ListBucketsHandler))
|
||||||
attachErrorHandler(defaultRouter)
|
attachErrorHandler(defaultRouter)
|
||||||
|
|
||||||
hr := NewHostBucketRouter("bucket")
|
vhsRouter := bucketRouter(cfg.Handler)
|
||||||
hr.Default(defaultRouter)
|
vhsRouter.Get("/", listWrapper(cfg))
|
||||||
for _, domain := range cfg.Domains {
|
router := newGlobalRouter(defaultRouter, vhsRouter)
|
||||||
hr.Map(domain, bucketRouter(cfg.Handler, cfg.Log))
|
|
||||||
}
|
api.Mount("/", router)
|
||||||
api.Mount("/", hr)
|
|
||||||
|
|
||||||
attachErrorHandler(api)
|
attachErrorHandler(api)
|
||||||
|
|
||||||
return api
|
return api
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type globalRouter struct {
|
||||||
|
pathStyleRouter chi.Router
|
||||||
|
vhsRouter chi.Router
|
||||||
|
}
|
||||||
|
|
||||||
|
func newGlobalRouter(pathStyleRouter, vhsRouter chi.Router) *globalRouter {
|
||||||
|
return &globalRouter{
|
||||||
|
pathStyleRouter: pathStyleRouter,
|
||||||
|
vhsRouter: vhsRouter,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *globalRouter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
router := g.pathStyleRouter
|
||||||
|
if reqInfo := s3middleware.GetReqInfo(r.Context()); reqInfo.RequestVHSEnabled {
|
||||||
|
router = g.vhsRouter
|
||||||
|
}
|
||||||
|
|
||||||
|
router.ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
func named(name string, handlerFunc http.HandlerFunc) http.HandlerFunc {
|
func named(name string, handlerFunc http.HandlerFunc) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
reqInfo := s3middleware.GetReqInfo(r.Context())
|
reqInfo := s3middleware.GetReqInfo(r.Context())
|
||||||
|
@ -178,6 +196,18 @@ func named(name string, handlerFunc http.HandlerFunc) http.HandlerFunc {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func listWrapper(cfg Config) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if reqInfo := s3middleware.GetReqInfo(r.Context()); reqInfo.BucketName == "" {
|
||||||
|
reqInfo.API = s3middleware.ListBucketsOperation
|
||||||
|
cfg.Handler.ListBucketsHandler(w, r)
|
||||||
|
} else {
|
||||||
|
reqInfo.API = s3middleware.ListObjectsV1Operation
|
||||||
|
cfg.Handler.ListObjectsV1Handler(w, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// If none of the http routes match respond with appropriate errors.
|
// If none of the http routes match respond with appropriate errors.
|
||||||
func errorResponseHandler(w http.ResponseWriter, r *http.Request) {
|
func errorResponseHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
|
@ -214,14 +244,13 @@ func attachErrorHandler(api *chi.Mux) {
|
||||||
api.MethodNotAllowed(named("MethodNotAllowed", errorHandler))
|
api.MethodNotAllowed(named("MethodNotAllowed", errorHandler))
|
||||||
}
|
}
|
||||||
|
|
||||||
func bucketRouter(h Handler, log *zap.Logger) chi.Router {
|
func bucketRouter(h Handler) chi.Router {
|
||||||
bktRouter := chi.NewRouter()
|
bktRouter := chi.NewRouter()
|
||||||
bktRouter.Use(
|
bktRouter.Use(
|
||||||
s3middleware.AddBucketName(log),
|
|
||||||
s3middleware.WrapHandler(h.AppendCORSHeaders),
|
s3middleware.WrapHandler(h.AppendCORSHeaders),
|
||||||
)
|
)
|
||||||
|
|
||||||
bktRouter.Mount("/", objectRouter(h, log))
|
bktRouter.Mount("/", objectRouter(h))
|
||||||
|
|
||||||
bktRouter.Options("/", named(s3middleware.OptionsBucketOperation, h.Preflight))
|
bktRouter.Options("/", named(s3middleware.OptionsBucketOperation, h.Preflight))
|
||||||
|
|
||||||
|
@ -368,9 +397,8 @@ func bucketRouter(h Handler, log *zap.Logger) chi.Router {
|
||||||
return bktRouter
|
return bktRouter
|
||||||
}
|
}
|
||||||
|
|
||||||
func objectRouter(h Handler, l *zap.Logger) chi.Router {
|
func objectRouter(h Handler) chi.Router {
|
||||||
objRouter := chi.NewRouter()
|
objRouter := chi.NewRouter()
|
||||||
objRouter.Use(s3middleware.AddObjectName(l))
|
|
||||||
|
|
||||||
objRouter.Options("/*", named(s3middleware.OptionsObjectOperation, h.Preflight))
|
objRouter.Options("/*", named(s3middleware.OptionsObjectOperation, h.Preflight))
|
||||||
|
|
||||||
|
|
|
@ -73,6 +73,9 @@ func (c *centerMock) Authenticate(*http.Request) (*middleware.Box, error) {
|
||||||
type middlewareSettingsMock struct {
|
type middlewareSettingsMock struct {
|
||||||
denyByDefault bool
|
denyByDefault bool
|
||||||
sourceIPHeader string
|
sourceIPHeader string
|
||||||
|
domains []string
|
||||||
|
vhsEnabled bool
|
||||||
|
vhsNamespacesEnabled map[string]bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *middlewareSettingsMock) SourceIPHeader() string {
|
func (r *middlewareSettingsMock) SourceIPHeader() string {
|
||||||
|
@ -91,6 +94,18 @@ func (r *middlewareSettingsMock) PolicyDenyByDefault() bool {
|
||||||
return r.denyByDefault
|
return r.denyByDefault
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *middlewareSettingsMock) Domains() []string {
|
||||||
|
return r.domains
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *middlewareSettingsMock) GlobalVHS() bool {
|
||||||
|
return r.vhsEnabled
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *middlewareSettingsMock) VHSNamespacesEnabled() map[string]bool {
|
||||||
|
return r.vhsNamespacesEnabled
|
||||||
|
}
|
||||||
|
|
||||||
type frostFSIDMock struct {
|
type frostFSIDMock struct {
|
||||||
tags map[string]string
|
tags map[string]string
|
||||||
validateError bool
|
validateError bool
|
||||||
|
|
|
@ -78,7 +78,6 @@ func prepareRouter(t *testing.T, opts ...option) *routerMock {
|
||||||
Metrics: metrics.NewAppMetrics(metricsConfig),
|
Metrics: metrics.NewAppMetrics(metricsConfig),
|
||||||
MiddlewareSettings: middlewareSettings,
|
MiddlewareSettings: middlewareSettings,
|
||||||
PolicyChecker: policyChecker,
|
PolicyChecker: policyChecker,
|
||||||
Domains: []string{"domain1", "domain2"},
|
|
||||||
FrostfsID: &frostFSIDMock{},
|
FrostfsID: &frostFSIDMock{},
|
||||||
XMLDecoder: &xmlMock{},
|
XMLDecoder: &xmlMock{},
|
||||||
Tagging: &resourceTaggingMock{},
|
Tagging: &resourceTaggingMock{},
|
||||||
|
|
|
@ -105,6 +105,9 @@ type (
|
||||||
policyDenyByDefault bool
|
policyDenyByDefault bool
|
||||||
sourceIPHeader string
|
sourceIPHeader string
|
||||||
retryMaxAttempts int
|
retryMaxAttempts int
|
||||||
|
domains []string
|
||||||
|
vhsEnabled bool
|
||||||
|
vhsNamespacesEnabled map[string]bool
|
||||||
retryMaxBackoff time.Duration
|
retryMaxBackoff time.Duration
|
||||||
retryStrategy handler.RetryStrategy
|
retryStrategy handler.RetryStrategy
|
||||||
}
|
}
|
||||||
|
@ -231,6 +234,7 @@ func (s *appSettings) update(v *viper.Viper, log *zap.Logger) {
|
||||||
s.setRetryMaxAttempts(fetchRetryMaxAttempts(v))
|
s.setRetryMaxAttempts(fetchRetryMaxAttempts(v))
|
||||||
s.setRetryMaxBackoff(fetchRetryMaxBackoff(v))
|
s.setRetryMaxBackoff(fetchRetryMaxBackoff(v))
|
||||||
s.setRetryStrategy(fetchRetryStrategy(v))
|
s.setRetryStrategy(fetchRetryStrategy(v))
|
||||||
|
s.setVHSSettings(v, log)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *appSettings) updateNamespacesSettings(v *viper.Viper, log *zap.Logger) {
|
func (s *appSettings) updateNamespacesSettings(v *viper.Viper, log *zap.Logger) {
|
||||||
|
@ -245,6 +249,39 @@ func (s *appSettings) updateNamespacesSettings(v *viper.Viper, log *zap.Logger)
|
||||||
s.namespaces = nsConfig.Namespaces
|
s.namespaces = nsConfig.Namespaces
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *appSettings) setVHSSettings(v *viper.Viper, log *zap.Logger) {
|
||||||
|
nsMap := fetchVHSNamespaces(v, log)
|
||||||
|
vhsNamespaces := make(map[string]bool, len(nsMap))
|
||||||
|
for ns, flag := range nsMap {
|
||||||
|
vhsNamespaces[s.ResolveNamespaceAlias(ns)] = flag
|
||||||
|
}
|
||||||
|
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
s.domains = fetchDomains(v)
|
||||||
|
s.vhsEnabled = v.GetBool(cfgVHSEnabled)
|
||||||
|
s.vhsNamespacesEnabled = vhsNamespaces
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *appSettings) Domains() []string {
|
||||||
|
s.mu.RLock()
|
||||||
|
defer s.mu.RUnlock()
|
||||||
|
return s.domains
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *appSettings) GlobalVHS() bool {
|
||||||
|
s.mu.RLock()
|
||||||
|
defer s.mu.RUnlock()
|
||||||
|
return s.vhsEnabled
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *appSettings) VHSNamespacesEnabled() map[string]bool {
|
||||||
|
s.mu.RLock()
|
||||||
|
defer s.mu.RUnlock()
|
||||||
|
return s.vhsNamespacesEnabled
|
||||||
|
}
|
||||||
|
|
||||||
func (s *appSettings) BypassContentEncodingInChunks() bool {
|
func (s *appSettings) BypassContentEncodingInChunks() bool {
|
||||||
s.mu.RLock()
|
s.mu.RLock()
|
||||||
defer s.mu.RUnlock()
|
defer s.mu.RUnlock()
|
||||||
|
@ -683,10 +720,6 @@ func (a *App) setHealthStatus() {
|
||||||
|
|
||||||
// Serve runs HTTP server to handle S3 API requests.
|
// Serve runs HTTP server to handle S3 API requests.
|
||||||
func (a *App) Serve(ctx context.Context) {
|
func (a *App) Serve(ctx context.Context) {
|
||||||
// Attach S3 API:
|
|
||||||
domains := a.cfg.GetStringSlice(cfgListenDomains)
|
|
||||||
a.log.Info(logs.FetchDomainsPrepareToUseAPI, zap.Strings("domains", domains))
|
|
||||||
|
|
||||||
cfg := api.Config{
|
cfg := api.Config{
|
||||||
Throttle: middleware.ThrottleOpts{
|
Throttle: middleware.ThrottleOpts{
|
||||||
Limit: a.settings.maxClient.count,
|
Limit: a.settings.maxClient.count,
|
||||||
|
@ -696,7 +729,6 @@ func (a *App) Serve(ctx context.Context) {
|
||||||
Center: a.ctr,
|
Center: a.ctr,
|
||||||
Log: a.log,
|
Log: a.log,
|
||||||
Metrics: a.metrics,
|
Metrics: a.metrics,
|
||||||
Domains: domains,
|
|
||||||
|
|
||||||
MiddlewareSettings: a.settings,
|
MiddlewareSettings: a.settings,
|
||||||
PolicyChecker: a.policyStorage,
|
PolicyChecker: a.policyStorage,
|
||||||
|
|
|
@ -144,6 +144,9 @@ const ( // Settings.
|
||||||
|
|
||||||
cfgListenDomains = "listen_domains"
|
cfgListenDomains = "listen_domains"
|
||||||
|
|
||||||
|
cfgVHSEnabled = "vhs.enabled"
|
||||||
|
cfgVHSNamespaces = "vhs.namespaces"
|
||||||
|
|
||||||
// Peers.
|
// Peers.
|
||||||
cfgPeers = "peers"
|
cfgPeers = "peers"
|
||||||
|
|
||||||
|
@ -667,6 +670,45 @@ func fetchServers(v *viper.Viper, log *zap.Logger) []ServerInfo {
|
||||||
return servers
|
return servers
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func fetchDomains(v *viper.Viper) []string {
|
||||||
|
domains := v.GetStringSlice(cfgListenDomains)
|
||||||
|
|
||||||
|
countParts := func(domain string) int {
|
||||||
|
return strings.Count(domain, ".")
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(domains, func(i, j int) bool {
|
||||||
|
return countParts(domains[i]) > countParts(domains[j])
|
||||||
|
})
|
||||||
|
|
||||||
|
return domains
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchVHSNamespaces(v *viper.Viper, log *zap.Logger) map[string]bool {
|
||||||
|
vhsNamespacesEnabled := make(map[string]bool)
|
||||||
|
nsMap := v.GetStringMap(cfgVHSNamespaces)
|
||||||
|
for ns, val := range nsMap {
|
||||||
|
if _, ok := vhsNamespacesEnabled[ns]; ok {
|
||||||
|
log.Warn(logs.WarnDuplicateNamespaceVHS, zap.String("namespace", ns))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
enabledFlag, ok := val.(bool)
|
||||||
|
if !ok {
|
||||||
|
log.Warn(logs.WarnValueVHSEnabledFlagWrongType, zap.String("namespace", ns))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
vhsNamespacesEnabled[ns] = enabledFlag
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(vhsNamespacesEnabled) > 0 {
|
||||||
|
return vhsNamespacesEnabled
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func newSettings() *viper.Viper {
|
func newSettings() *viper.Viper {
|
||||||
v := viper.New()
|
v := viper.New()
|
||||||
|
|
||||||
|
|
|
@ -36,8 +36,11 @@ S3_GW_SERVER_1_TLS_KEY_FILE=/path/to/tls/key
|
||||||
# How often to reconnect to the servers
|
# How often to reconnect to the servers
|
||||||
S3_GW_RECONNECT_INTERVAL: 1m
|
S3_GW_RECONNECT_INTERVAL: 1m
|
||||||
|
|
||||||
# Domains to be able to use virtual-hosted-style access to bucket.
|
# Domains to be able to use virtual-hosted-style access to bucket
|
||||||
S3_GW_LISTEN_DOMAINS=s3dev.frostfs.devenv
|
S3_GW_LISTEN_DOMAINS="domain.com <wildcard>.domain.com"
|
||||||
|
|
||||||
|
# VHS enabled flag
|
||||||
|
S3_GW_VHS_ENABLED=false
|
||||||
|
|
||||||
# Config file
|
# Config file
|
||||||
S3_GW_CONFIG=/path/to/config/yaml
|
S3_GW_CONFIG=/path/to/config/yaml
|
||||||
|
|
|
@ -42,6 +42,13 @@ server:
|
||||||
# Domains to be able to use virtual-hosted-style access to bucket.
|
# Domains to be able to use virtual-hosted-style access to bucket.
|
||||||
listen_domains:
|
listen_domains:
|
||||||
- s3dev.frostfs.devenv
|
- s3dev.frostfs.devenv
|
||||||
|
- s3dev.<wildcard>.frostfs.devenv
|
||||||
|
|
||||||
|
vhs:
|
||||||
|
enabled: false
|
||||||
|
namespaces:
|
||||||
|
"ns1": false
|
||||||
|
"ns2": true
|
||||||
|
|
||||||
logger:
|
logger:
|
||||||
level: debug
|
level: debug
|
||||||
|
|
|
@ -193,12 +193,14 @@ There are some custom types used for brevity:
|
||||||
| `namespaces` | [Namespaces configuration](#namespaces-section) |
|
| `namespaces` | [Namespaces configuration](#namespaces-section) |
|
||||||
| `retry` | [Retry configuration](#retry-section) |
|
| `retry` | [Retry configuration](#retry-section) |
|
||||||
| `containers` | [Containers configuration](#containers-section) |
|
| `containers` | [Containers configuration](#containers-section) |
|
||||||
|
| `vhs` | [VHS configuration](#vhs-section) |
|
||||||
|
|
||||||
### General section
|
### General section
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
listen_domains:
|
listen_domains:
|
||||||
- s3dev.frostfs.devenv
|
- s3dev.frostfs.devenv
|
||||||
|
- s3dev.<wildcard>.frostfs.devenv
|
||||||
- s3dev2.frostfs.devenv
|
- s3dev2.frostfs.devenv
|
||||||
|
|
||||||
rpc_endpoint: http://morph-chain.frostfs.devenv:30333
|
rpc_endpoint: http://morph-chain.frostfs.devenv:30333
|
||||||
|
@ -226,7 +228,7 @@ source_ip_header: "Source-Ip"
|
||||||
|
|
||||||
| Parameter | Type | SIGHUP reload | Default value | Description |
|
| Parameter | Type | SIGHUP reload | Default value | Description |
|
||||||
|----------------------------------|------------|---------------|---------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|----------------------------------|------------|---------------|---------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| `listen_domains` | `[]string` | no | | Domains to be able to use virtual-hosted-style access to bucket. |
|
| `listen_domains` | `[]string` | yes | | Domains to be able to use virtual-hosted-style access to bucket. The presence of placeholders of the <wildcard> type is supported. |
|
||||||
| `rpc_endpoint` | `string` | no | | The address of the RPC host to which the gateway connects to resolve bucket names and interact with frostfs contracts (required to use the `nns` resolver and `frostfsid` contract). |
|
| `rpc_endpoint` | `string` | no | | The address of the RPC host to which the gateway connects to resolve bucket names and interact with frostfs contracts (required to use the `nns` resolver and `frostfsid` contract). |
|
||||||
| `resolve_order` | `[]string` | yes | `[dns]` | Order of bucket name resolvers to use. Available resolvers: `dns`, `nns`. |
|
| `resolve_order` | `[]string` | yes | `[dns]` | Order of bucket name resolvers to use. Available resolvers: `dns`, `nns`. |
|
||||||
| `connect_timeout` | `duration` | no | `10s` | Timeout to connect to a node. |
|
| `connect_timeout` | `duration` | no | `10s` | Timeout to connect to a node. |
|
||||||
|
@ -721,3 +723,20 @@ containers:
|
||||||
| Parameter | Type | SIGHUP reload | Default value | Description |
|
| Parameter | Type | SIGHUP reload | Default value | Description |
|
||||||
|-----------|----------|---------------|---------------|--------------------------------------------------------------------------------------|
|
|-----------|----------|---------------|---------------|--------------------------------------------------------------------------------------|
|
||||||
| `cors` | `string` | no | | Container name for CORS configurations. If not set, container of the bucket is used. |
|
| `cors` | `string` | no | | Container name for CORS configurations. If not set, container of the bucket is used. |
|
||||||
|
|
||||||
|
# `vhs` section
|
||||||
|
|
||||||
|
Configuration of virtual hosted addressing style.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
vhs:
|
||||||
|
enabled: false
|
||||||
|
namespaces:
|
||||||
|
"ns1": false
|
||||||
|
"ns2": true
|
||||||
|
```
|
||||||
|
|
||||||
|
| Parameter | Type | SIGHUP reload | Default value | Description |
|
||||||
|
|--------------|-------------------|---------------|---------------|----------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
|
| `enabled` | `bool` | yes | `false` | Enables the use of virtual host addressing for banquets at the application level. |
|
||||||
|
| `namespaces` | `map[string]bool` | yes | | A map in which the keys are the name of the namespace, and the values are the flag responsible for enabling VHS for the specified namespace. |
|
||||||
|
|
|
@ -20,7 +20,6 @@ const (
|
||||||
UsingCredentials = "using credentials" // Info in ../../cmd/s3-gw/app.go
|
UsingCredentials = "using credentials" // Info in ../../cmd/s3-gw/app.go
|
||||||
ApplicationStarted = "application started" // Info in ../../cmd/s3-gw/app.go
|
ApplicationStarted = "application started" // Info in ../../cmd/s3-gw/app.go
|
||||||
ApplicationFinished = "application finished" // Info in ../../cmd/s3-gw/app.go
|
ApplicationFinished = "application finished" // Info in ../../cmd/s3-gw/app.go
|
||||||
FetchDomainsPrepareToUseAPI = "fetch domains, prepare to use API" // Info in ../../cmd/s3-gw/app.go
|
|
||||||
StartingServer = "starting server" // Info in ../../cmd/s3-gw/app.go
|
StartingServer = "starting server" // Info in ../../cmd/s3-gw/app.go
|
||||||
StoppingServer = "stopping server" // Info in ../../cmd/s3-gw/app.go
|
StoppingServer = "stopping server" // Info in ../../cmd/s3-gw/app.go
|
||||||
SIGHUPConfigReloadStarted = "SIGHUP config reload started" // Info in ../../cmd/s3-gw/app.go
|
SIGHUPConfigReloadStarted = "SIGHUP config reload started" // Info in ../../cmd/s3-gw/app.go
|
||||||
|
@ -100,7 +99,6 @@ const (
|
||||||
FailedToPassAuthentication = "failed to pass authentication" // Error in ../../api/middleware/auth.go
|
FailedToPassAuthentication = "failed to pass authentication" // Error in ../../api/middleware/auth.go
|
||||||
FailedToResolveCID = "failed to resolve CID" // Debug in ../../api/middleware/metrics.go
|
FailedToResolveCID = "failed to resolve CID" // Debug in ../../api/middleware/metrics.go
|
||||||
RequestStart = "request start" // Info in ../../api/middleware/reqinfo.go
|
RequestStart = "request start" // Info in ../../api/middleware/reqinfo.go
|
||||||
FailedToUnescapeObjectName = "failed to unescape object name" // Warn in ../../api/middleware/reqinfo.go
|
|
||||||
InvalidDefaultMaxAge = "invalid defaultMaxAge" // Fatal in ../../cmd/s3-gw/app_settings.go
|
InvalidDefaultMaxAge = "invalid defaultMaxAge" // Fatal in ../../cmd/s3-gw/app_settings.go
|
||||||
CantShutDownService = "can't shut down service" // Panic in ../../cmd/s3-gw/service.go
|
CantShutDownService = "can't shut down service" // Panic in ../../cmd/s3-gw/service.go
|
||||||
CouldntGenerateRandomKey = "couldn't generate random key" // Fatal in ../../cmd/s3-gw/app.go
|
CouldntGenerateRandomKey = "couldn't generate random key" // Fatal in ../../cmd/s3-gw/app.go
|
||||||
|
@ -154,4 +152,6 @@ const (
|
||||||
FailedToParsePartInfo = "failed to parse part info"
|
FailedToParsePartInfo = "failed to parse part info"
|
||||||
CouldNotFetchCORSContainerInfo = "couldn't fetch CORS container info"
|
CouldNotFetchCORSContainerInfo = "couldn't fetch CORS container info"
|
||||||
CloseCredsObjectPayload = "close creds object payload"
|
CloseCredsObjectPayload = "close creds object payload"
|
||||||
|
WarnDuplicateNamespaceVHS = "duplicate namespace with enabled VHS, config value skipped"
|
||||||
|
WarnValueVHSEnabledFlagWrongType = "the value of the VHS enable flag for the namespace is of the wrong type, config value skipped"
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in a new issue