forked from TrueCloudLab/frostfs-http-gw
[#158] Support cors
Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
This commit is contained in:
parent
901b8ff95b
commit
46c63edd67
6 changed files with 201 additions and 16 deletions
|
@ -7,6 +7,7 @@ This document outlines major changes between releases.
|
|||
### Added
|
||||
- Support percent-encoding for GET queries (#134)
|
||||
- Add `trace_id` to logs (#148)
|
||||
- Add `cors` config params (#158)
|
||||
|
||||
### Changed
|
||||
- Update go version to 1.22 (#132)
|
||||
|
|
|
@ -11,6 +11,7 @@ import (
|
|||
"os/signal"
|
||||
"runtime/debug"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
@ -87,15 +88,30 @@ type (
|
|||
appSettings struct {
|
||||
reconnectInterval time.Duration
|
||||
|
||||
mu sync.RWMutex
|
||||
defaultTimestamp bool
|
||||
zipCompression bool
|
||||
clientCut bool
|
||||
returnIndexPage bool
|
||||
indexPageTemplate string
|
||||
bufferMaxSizeForPut uint64
|
||||
namespaceHeader string
|
||||
defaultNamespaces []string
|
||||
mu sync.RWMutex
|
||||
defaultTimestamp bool
|
||||
zipCompression bool
|
||||
clientCut bool
|
||||
returnIndexPage bool
|
||||
indexPageTemplate string
|
||||
bufferMaxSizeForPut uint64
|
||||
namespaceHeader string
|
||||
defaultNamespaces []string
|
||||
corsAllowOrigin string
|
||||
corsAllowMethods []string
|
||||
corsAllowHeaders []string
|
||||
corsExposeHeaders []string
|
||||
corsAllowCredentials bool
|
||||
corsMaxAge int
|
||||
}
|
||||
|
||||
CORS struct {
|
||||
AllowOrigin string
|
||||
AllowMethods []string
|
||||
AllowHeaders []string
|
||||
ExposeHeaders []string
|
||||
AllowCredentials bool
|
||||
MaxAge int
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -177,6 +193,12 @@ func (s *appSettings) update(v *viper.Viper, l *zap.Logger) {
|
|||
namespaceHeader := v.GetString(cfgResolveNamespaceHeader)
|
||||
defaultNamespaces := fetchDefaultNamespaces(v)
|
||||
indexPage, indexEnabled := fetchIndexPageTemplate(v, l)
|
||||
corsAllowOrigin := v.GetString(cfgCORSAllowOrigin)
|
||||
corsAllowMethods := v.GetStringSlice(cfgCORSAllowMethods)
|
||||
corsAllowHeaders := v.GetStringSlice(cfgCORSAllowHeaders)
|
||||
corsExposeHeaders := v.GetStringSlice(cfgCORSExposeHeaders)
|
||||
corsAllowCredentials := v.GetBool(cfgCORSAllowCredentials)
|
||||
corsMaxAge := fetchCORSMaxAge(v)
|
||||
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
@ -190,6 +212,12 @@ func (s *appSettings) update(v *viper.Viper, l *zap.Logger) {
|
|||
s.defaultNamespaces = defaultNamespaces
|
||||
s.returnIndexPage = indexEnabled
|
||||
s.indexPageTemplate = indexPage
|
||||
s.corsAllowOrigin = corsAllowOrigin
|
||||
s.corsAllowMethods = corsAllowMethods
|
||||
s.corsAllowHeaders = corsAllowHeaders
|
||||
s.corsExposeHeaders = corsExposeHeaders
|
||||
s.corsAllowCredentials = corsAllowCredentials
|
||||
s.corsMaxAge = corsMaxAge
|
||||
}
|
||||
|
||||
func (s *appSettings) DefaultTimestamp() bool {
|
||||
|
@ -219,6 +247,29 @@ func (s *appSettings) IndexPageTemplate() string {
|
|||
return s.indexPageTemplate
|
||||
}
|
||||
|
||||
func (s *appSettings) CORS() CORS {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
allowMethods := make([]string, len(s.corsAllowMethods))
|
||||
copy(allowMethods, s.corsAllowMethods)
|
||||
|
||||
allowHeaders := make([]string, len(s.corsAllowHeaders))
|
||||
copy(allowHeaders, s.corsAllowHeaders)
|
||||
|
||||
exposeHeaders := make([]string, len(s.corsExposeHeaders))
|
||||
copy(exposeHeaders, s.corsExposeHeaders)
|
||||
|
||||
return CORS{
|
||||
AllowOrigin: s.corsAllowOrigin,
|
||||
AllowMethods: allowMethods,
|
||||
AllowHeaders: allowHeaders,
|
||||
ExposeHeaders: exposeHeaders,
|
||||
AllowCredentials: s.corsAllowCredentials,
|
||||
MaxAge: s.corsMaxAge,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *appSettings) ClientCut() bool {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
@ -550,7 +601,6 @@ func (a *app) stopServices() {
|
|||
svc.ShutDown(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *app) configureRouter(handler *handler.Handler) {
|
||||
r := router.New()
|
||||
r.RedirectTrailingSlash = true
|
||||
|
@ -561,20 +611,96 @@ func (a *app) configureRouter(handler *handler.Handler) {
|
|||
response.Error(r, "Method Not Allowed", fasthttp.StatusMethodNotAllowed)
|
||||
}
|
||||
|
||||
r.POST("/upload/{cid}", a.tracer(a.logger(a.canonicalizer(a.tokenizer(a.reqNamespace(handler.Upload))))))
|
||||
r.POST("/upload/{cid}", a.addMiddlewares(handler.Upload))
|
||||
r.OPTIONS("/upload/{cid}", a.addPreflight())
|
||||
a.log.Info(logs.AddedPathUploadCid)
|
||||
r.GET("/get/{cid}/{oid:*}", a.tracer(a.logger(a.canonicalizer(a.tokenizer(a.reqNamespace(handler.DownloadByAddressOrBucketName))))))
|
||||
r.HEAD("/get/{cid}/{oid:*}", a.tracer(a.logger(a.canonicalizer(a.tokenizer(a.reqNamespace(handler.HeadByAddressOrBucketName))))))
|
||||
r.GET("/get/{cid}/{oid:*}", a.addMiddlewares(handler.DownloadByAddressOrBucketName))
|
||||
r.HEAD("/get/{cid}/{oid:*}", a.addMiddlewares(handler.HeadByAddressOrBucketName))
|
||||
r.OPTIONS("/get/{cid}/{oid:*}", a.addPreflight())
|
||||
a.log.Info(logs.AddedPathGetCidOid)
|
||||
r.GET("/get_by_attribute/{cid}/{attr_key}/{attr_val:*}", a.tracer(a.logger(a.canonicalizer(a.tokenizer(a.reqNamespace(handler.DownloadByAttribute))))))
|
||||
r.HEAD("/get_by_attribute/{cid}/{attr_key}/{attr_val:*}", a.tracer(a.logger(a.canonicalizer(a.tokenizer(a.reqNamespace(handler.HeadByAttribute))))))
|
||||
r.GET("/get_by_attribute/{cid}/{attr_key}/{attr_val:*}", a.addMiddlewares(handler.DownloadByAttribute))
|
||||
r.HEAD("/get_by_attribute/{cid}/{attr_key}/{attr_val:*}", a.addMiddlewares(handler.HeadByAttribute))
|
||||
r.OPTIONS("/get_by_attribute/{cid}/{attr_key}/{attr_val:*}", a.addPreflight())
|
||||
a.log.Info(logs.AddedPathGetByAttributeCidAttrKeyAttrVal)
|
||||
r.GET("/zip/{cid}/{prefix:*}", a.tracer(a.logger(a.canonicalizer(a.tokenizer(a.reqNamespace(handler.DownloadZipped))))))
|
||||
r.GET("/zip/{cid}/{prefix:*}", a.addMiddlewares(handler.DownloadZipped))
|
||||
r.OPTIONS("/zip/{cid}/{prefix:*}", a.addPreflight())
|
||||
a.log.Info(logs.AddedPathZipCidPrefix)
|
||||
|
||||
a.webServer.Handler = r.Handler
|
||||
}
|
||||
|
||||
func (a *app) addMiddlewares(h fasthttp.RequestHandler) fasthttp.RequestHandler {
|
||||
list := []func(fasthttp.RequestHandler) fasthttp.RequestHandler{
|
||||
a.tracer,
|
||||
a.logger,
|
||||
a.canonicalizer,
|
||||
a.tokenizer,
|
||||
a.reqNamespace,
|
||||
a.cors,
|
||||
}
|
||||
|
||||
for i := len(list) - 1; i >= 0; i-- {
|
||||
h = list[i](h)
|
||||
}
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
func (a *app) addPreflight() fasthttp.RequestHandler {
|
||||
list := []func(fasthttp.RequestHandler) fasthttp.RequestHandler{
|
||||
a.tracer,
|
||||
a.logger,
|
||||
a.reqNamespace,
|
||||
}
|
||||
|
||||
h := a.preflightHandler
|
||||
for i := len(list) - 1; i >= 0; i-- {
|
||||
h = list[i](h)
|
||||
}
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
func (a *app) preflightHandler(c *fasthttp.RequestCtx) {
|
||||
cors := a.settings.CORS()
|
||||
setCORSHeaders(c, cors)
|
||||
}
|
||||
|
||||
func (a *app) cors(h fasthttp.RequestHandler) fasthttp.RequestHandler {
|
||||
return func(c *fasthttp.RequestCtx) {
|
||||
h(c)
|
||||
code := c.Response.StatusCode()
|
||||
if code >= fasthttp.StatusOK && code < fasthttp.StatusMultipleChoices {
|
||||
cors := a.settings.CORS()
|
||||
setCORSHeaders(c, cors)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func setCORSHeaders(c *fasthttp.RequestCtx, cors CORS) {
|
||||
c.Response.Header.Set(fasthttp.HeaderAccessControlMaxAge, strconv.Itoa(cors.MaxAge))
|
||||
|
||||
if len(cors.AllowOrigin) != 0 {
|
||||
c.Response.Header.Set(fasthttp.HeaderAccessControlAllowOrigin, cors.AllowOrigin)
|
||||
}
|
||||
|
||||
if len(cors.AllowMethods) != 0 {
|
||||
c.Response.Header.Set(fasthttp.HeaderAccessControlAllowMethods, strings.Join(cors.AllowMethods, ","))
|
||||
}
|
||||
|
||||
if len(cors.AllowHeaders) != 0 {
|
||||
c.Response.Header.Set(fasthttp.HeaderAccessControlAllowHeaders, strings.Join(cors.AllowHeaders, ","))
|
||||
}
|
||||
|
||||
if len(cors.ExposeHeaders) != 0 {
|
||||
c.Response.Header.Set(fasthttp.HeaderAccessControlExposeHeaders, strings.Join(cors.ExposeHeaders, ","))
|
||||
}
|
||||
|
||||
if cors.AllowCredentials {
|
||||
c.Response.Header.Set(fasthttp.HeaderAccessControlAllowCredentials, "true")
|
||||
}
|
||||
}
|
||||
|
||||
func (a *app) logger(h fasthttp.RequestHandler) fasthttp.RequestHandler {
|
||||
return func(req *fasthttp.RequestCtx) {
|
||||
requiredFields := []zap.Field{zap.Uint64("id", req.ID())}
|
||||
|
|
|
@ -56,6 +56,8 @@ const (
|
|||
|
||||
defaultReconnectInterval = time.Minute
|
||||
|
||||
defaultCORSMaxAge = 600 // seconds
|
||||
|
||||
cfgServer = "server"
|
||||
cfgTLSEnabled = "tls.enabled"
|
||||
cfgTLSCertFile = "tls.cert_file"
|
||||
|
@ -141,6 +143,14 @@ const (
|
|||
cfgResolveNamespaceHeader = "resolve_bucket.namespace_header"
|
||||
cfgResolveDefaultNamespaces = "resolve_bucket.default_namespaces"
|
||||
|
||||
// CORS.
|
||||
cfgCORSAllowOrigin = "cors.allow_origin"
|
||||
cfgCORSAllowMethods = "cors.allow_methods"
|
||||
cfgCORSAllowHeaders = "cors.allow_headers"
|
||||
cfgCORSExposeHeaders = "cors.expose_headers"
|
||||
cfgCORSAllowCredentials = "cors.allow_credentials"
|
||||
cfgCORSMaxAge = "cors.max_age"
|
||||
|
||||
// Command line args.
|
||||
cmdHelp = "help"
|
||||
cmdVersion = "version"
|
||||
|
@ -537,6 +547,15 @@ func fetchDefaultNamespaces(v *viper.Viper) []string {
|
|||
return namespaces
|
||||
}
|
||||
|
||||
func fetchCORSMaxAge(v *viper.Viper) int {
|
||||
maxAge := v.GetInt(cfgCORSMaxAge)
|
||||
if maxAge <= 0 {
|
||||
maxAge = defaultCORSMaxAge
|
||||
}
|
||||
|
||||
return maxAge
|
||||
}
|
||||
|
||||
func fetchServers(v *viper.Viper, log *zap.Logger) []ServerInfo {
|
||||
var servers []ServerInfo
|
||||
seen := make(map[string]struct{})
|
||||
|
|
|
@ -126,3 +126,10 @@ HTTP_GW_RESOLVE_BUCKET_DEFAULT_NAMESPACES="" "root"
|
|||
# Max attempt to make successful tree request.
|
||||
# default value is 0 that means the number of attempts equals to number of nodes in pool.
|
||||
HTTP_GW_FROSTFS_TREE_POOL_MAX_ATTEMPTS=0
|
||||
|
||||
HTTP_GW_CORS_ALLOW_ORIGIN="*"
|
||||
HTTP_GW_CORS_ALLOW_METHODS="GET" "POST"
|
||||
HTTP_GW_CORS_ALLOW_HEADERS="*"
|
||||
HTTP_GW_CORS_EXPOSE_HEADERS="*"
|
||||
HTTP_GW_CORS_ALLOW_CREDENTIALS=false
|
||||
HTTP_GW_CORS_MAX_AGE=600
|
||||
|
|
|
@ -138,3 +138,11 @@ cache:
|
|||
resolve_bucket:
|
||||
namespace_header: X-Frostfs-Namespace
|
||||
default_namespaces: [ "", "root" ]
|
||||
|
||||
cors:
|
||||
allow_origin: ""
|
||||
allow_methods: []
|
||||
allow_headers: []
|
||||
expose_headers: []
|
||||
allow_credentials: false
|
||||
max_age: 600
|
||||
|
|
|
@ -363,3 +363,27 @@ index_page:
|
|||
|-----------------|----------|---------------|---------------|---------------------------------------------------------------------------------|
|
||||
| `enabled` | `bool` | yes | `false` | Flag to enable index_page return if no object with specified S3-name was found. |
|
||||
| `template_path` | `string` | yes | `""` | Path to .gotmpl file with html template for index_page. |
|
||||
|
||||
# `cors` section
|
||||
|
||||
Parameters for CORS (used in OPTIONS requests and responses in all handlers).
|
||||
If values are not set, headers will not be included to response.
|
||||
|
||||
```yaml
|
||||
cors:
|
||||
allow_origin: "*"
|
||||
allow_methods: ["GET", "HEAD"]
|
||||
allow_headers: ["Authorization"]
|
||||
expose_headers: ["*"]
|
||||
allow_credentials: false
|
||||
max_age: 600
|
||||
```
|
||||
|
||||
| Parameter | Type | SIGHUP reload | Default value | Description |
|
||||
|---------------------|------------|---------------|---------------|--------------------------------------------------------|
|
||||
| `allow_origin` | `string` | yes | | Values for `Access-Control-Allow-Origin` headers. |
|
||||
| `allow_methods` | `[]string` | yes | | Values for `Access-Control-Allow-Methods` headers. |
|
||||
| `allow_headers` | `[]string` | yes | | Values for `Access-Control-Allow-Headers` headers. |
|
||||
| `expose_headers` | `[]string` | yes | | Values for `Access-Control-Expose-Headers` headers. |
|
||||
| `allow_credentials` | `bool` | yes | `false` | Values for `Access-Control-Allow-Credentials` headers. |
|
||||
| `max_age` | `int` | yes | `600` | Values for `Access-Control-Max-Age ` headers. |
|
||||
|
|
Loading…
Reference in a new issue