From 239397f86c0cbb24e0ac4ab6c9003f1f973f323a Mon Sep 17 00:00:00 2001 From: Denis Kirillov Date: Thu, 24 Oct 2024 17:27:32 +0300 Subject: [PATCH] [#158] Support cors Signed-off-by: Denis Kirillov --- CHANGELOG.md | 1 + cmd/http-gw/app.go | 68 +++++++++++++++++++++++++++++++++----- cmd/http-gw/settings.go | 19 +++++++++++ config/config.env | 7 ++++ config/config.yaml | 8 +++++ docs/gate-configuration.md | 24 ++++++++++++++ 6 files changed, 118 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 46e9c23..0990b51 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/cmd/http-gw/app.go b/cmd/http-gw/app.go index 8820c69..fdf1329 100644 --- a/cmd/http-gw/app.go +++ b/cmd/http-gw/app.go @@ -88,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 } ) @@ -178,6 +193,12 @@ func (s *appSettings) update(v *viper.Viper, l *zap.Logger) { namespaceHeader := v.GetString(cfgResolveNamespaceHeader) defaultNamespaces := v.GetStringSlice(cfgResolveDefaultNamespaces) 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() @@ -191,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 { @@ -220,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() diff --git a/cmd/http-gw/settings.go b/cmd/http-gw/settings.go index fb1afab..39d47b3 100644 --- a/cmd/http-gw/settings.go +++ b/cmd/http-gw/settings.go @@ -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" @@ -527,6 +537,15 @@ func fetchIndexPageTemplate(v *viper.Viper, l *zap.Logger) (string, bool) { return string(tmpl), true } +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{}) diff --git a/config/config.env b/config/config.env index e1d5e7d..d2f4a56 100644 --- a/config/config.env +++ b/config/config.env @@ -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 diff --git a/config/config.yaml b/config/config.yaml index 61aa70b..dd985ad 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -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 diff --git a/docs/gate-configuration.md b/docs/gate-configuration.md index b484f9d..7a3eba7 100644 --- a/docs/gate-configuration.md +++ b/docs/gate-configuration.md @@ -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. |