diff --git a/changelog/unreleased/make-tusd-cors-configurable.md b/changelog/unreleased/make-tusd-cors-configurable.md new file mode 100644 index 0000000000..3d808ecf41 --- /dev/null +++ b/changelog/unreleased/make-tusd-cors-configurable.md @@ -0,0 +1,5 @@ +Bugfix: make tusd CORS headers configurable + +We bumped tusd to 1.13.0 and made CORS headers configurable via mapstructure. + +https://github.com/cs3org/reva/pull/4507 diff --git a/internal/http/services/datagateway/datagateway.go b/internal/http/services/datagateway/datagateway.go index 2a571d2cf4..13411fc36d 100644 --- a/internal/http/services/datagateway/datagateway.go +++ b/internal/http/services/datagateway/datagateway.go @@ -135,7 +135,6 @@ func (s *svc) setHandler() { r = r.WithContext(ctx) switch r.Method { case "HEAD": - addCorsHeader(w) s.doHead(w, r) return case "GET": @@ -147,6 +146,9 @@ func (s *svc) setHandler() { case "PATCH": s.doPatch(w, r) return + case "OPTIONS": + s.doOptions(w, r) + return default: w.WriteHeader(http.StatusNotImplemented) return @@ -154,13 +156,6 @@ func (s *svc) setHandler() { }) } -func addCorsHeader(res http.ResponseWriter) { - headers := res.Header() - headers.Set("Access-Control-Allow-Origin", "*") - headers.Set("Access-Control-Allow-Headers", "Content-Type, Origin, Authorization") - headers.Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS, HEAD") -} - func (s *svc) verify(ctx context.Context, r *http.Request) (*transferClaims, error) { // Extract transfer token from request header. If not existing, assume that it's the last path segment instead. token := r.Header.Get(TokenTransportHeader) @@ -408,6 +403,52 @@ func (s *svc) doPatch(w http.ResponseWriter, r *http.Request) { } } +func (s *svc) doOptions(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + log := appctx.GetLogger(ctx) + + claims, err := s.verify(ctx, r) + if err != nil { + err = errors.Wrap(err, "datagateway: error validating transfer token") + log.Error().Err(err).Str("token", r.Header.Get(TokenTransportHeader)).Msg("invalid transfer token") + w.WriteHeader(http.StatusForbidden) + return + } + + log.Debug().Str("target", claims.Target).Msg("sending request to internal data server") + + httpClient := s.client + httpReq, err := rhttp.NewRequest(ctx, "OPTIONS", claims.Target, nil) + if err != nil { + log.Error().Err(err).Msg("wrong request") + w.WriteHeader(http.StatusInternalServerError) + return + } + httpReq.Header = r.Header + + httpRes, err := httpClient.Do(httpReq) + if err != nil { + log.Error().Err(err).Msg("error doing OPTIONS request to data service") + w.WriteHeader(http.StatusInternalServerError) + return + } + defer httpRes.Body.Close() + + copyHeader(w.Header(), httpRes.Header) + + // add upload expiry / transfer token expiry header for tus https://tus.io/protocols/resumable-upload.html#expiration + w.Header().Set(UploadExpiresHeader, time.Unix(claims.ExpiresAt, 0).Format(time.RFC1123)) + + if httpRes.StatusCode != http.StatusOK { + // swallow the body and set content-length to 0 to prevent reverse proxies from trying to read from it + w.Header().Set("Content-Length", "0") + w.WriteHeader(httpRes.StatusCode) + return + } + + w.WriteHeader(http.StatusOK) +} + func copyHeader(dst, src http.Header) { for key, values := range src { for i := range values { diff --git a/pkg/rhttp/datatx/manager/tus/tus.go b/pkg/rhttp/datatx/manager/tus/tus.go index 6da1740154..e9b2a07b03 100644 --- a/pkg/rhttp/datatx/manager/tus/tus.go +++ b/pkg/rhttp/datatx/manager/tus/tus.go @@ -23,6 +23,7 @@ import ( "log" "net/http" "path" + "regexp" "github.com/pkg/errors" tusd "github.com/tus/tusd/pkg/handler" @@ -45,14 +46,25 @@ func init() { registry.Register("tus", New) } +type TusConfig struct { + cache.Config + CorsEnabled bool `mapstructure:"cors_enabled"` + CorsAllowOrigin string `mapstructure:"cors_allow_origin"` + CorsAllowCredentials bool `mapstructure:"cors_allow_credentials"` + CorsAllowMethods string `mapstructure:"cors_allow_methods"` + CorsAllowHeaders string `mapstructure:"cors_allow_headers"` + CorsMaxAge string `mapstructure:"cors_max_age"` + CorsExposeHeaders string `mapstructure:"cors_expose_headers"` +} + type manager struct { - conf *cache.Config + conf *TusConfig publisher events.Publisher statCache cache.StatCache } -func parseConfig(m map[string]interface{}) (*cache.Config, error) { - c := &cache.Config{} +func parseConfig(m map[string]interface{}) (*TusConfig, error) { + c := &TusConfig{} if err := mapstructure.Decode(m, c); err != nil { err = errors.Wrap(err, "error decoding conf") return nil, err @@ -69,7 +81,7 @@ func New(m map[string]interface{}, publisher events.Publisher) (datatx.DataTX, e return &manager{ conf: c, publisher: publisher, - statCache: cache.GetStatCache(*c), + statCache: cache.GetStatCache(c.Config), }, nil } @@ -94,6 +106,23 @@ func (m *manager) Handler(fs storage.FS) (http.Handler, error) { Logger: log.New(appctx.GetLogger(context.Background()), "", 0), } + if m.conf.CorsEnabled { + allowOrigin, err := regexp.Compile(m.conf.CorsAllowOrigin) + if m.conf.CorsAllowOrigin != "" && err != nil { + return nil, err + } + + config.Cors = &tusd.CorsConfig{ + Disable: false, + AllowOrigin: allowOrigin, + AllowCredentials: m.conf.CorsAllowCredentials, + AllowMethods: m.conf.CorsAllowMethods, + AllowHeaders: m.conf.CorsAllowHeaders, + MaxAge: m.conf.CorsMaxAge, + ExposeHeaders: m.conf.CorsExposeHeaders, + } + } + handler, err := tusd.NewUnroutedHandler(config) if err != nil { return nil, err