diff --git a/backend/config/config.go b/backend/config/config.go index 9e7362f..093f2e8 100644 --- a/backend/config/config.go +++ b/backend/config/config.go @@ -41,6 +41,7 @@ type Config struct { S3Bucket string AssetsHTTPRoot string FFmpegWorkerPoolSize int + CORSAllowedOrigins []string } const Prefix = "CLIPPER_" @@ -141,6 +142,12 @@ func NewFromEnv() (Config, error) { } } + var corsAllowedOrigins []string + corsAllowedOriginsName := envPrefix("CORS_ALLOWED_ORIGINS") + if s := os.Getenv(corsAllowedOriginsName); s != "" { + corsAllowedOrigins = strings.Split(s, ",") + } + return Config{ Environment: env, BindAddr: bindAddr, @@ -156,5 +163,6 @@ func NewFromEnv() (Config, error) { FileStoreHTTPRoot: fileStoreHTTPRoot, FileStoreHTTPBaseURL: fileStoreHTTPBaseURL, FFmpegWorkerPoolSize: ffmpegWorkerPoolSize, + CORSAllowedOrigins: corsAllowedOrigins, }, nil } diff --git a/backend/config/config_test.go b/backend/config/config_test.go index 0b6158f..aba6750 100644 --- a/backend/config/config_test.go +++ b/backend/config/config_test.go @@ -271,4 +271,24 @@ func TestNewFromEnv(t *testing.T) { assert.Equal(t, "eu-west-1", c.AWSRegion) assert.Equal(t, "bucket", c.S3Bucket) }) + + t.Run("CORS_ALLOWED_ORIGINS", func(t *testing.T) { + defer clearenv() + setupenv() + + os.Setenv("CLIPPER_CORS_ALLOWED_ORIGINS", "") + c, err := config.NewFromEnv() + require.NoError(t, err) + assert.Nil(t, c.CORSAllowedOrigins) + + os.Setenv("CLIPPER_CORS_ALLOWED_ORIGINS", "*") + c, err = config.NewFromEnv() + require.NoError(t, err) + assert.Equal(t, []string{"*"}, c.CORSAllowedOrigins) + + os.Setenv("CLIPPER_CORS_ALLOWED_ORIGINS", "https://www1.example.com,https://www2.example.com") + c, err = config.NewFromEnv() + require.NoError(t, err) + assert.Equal(t, []string{"https://www1.example.com", "https://www2.example.com"}, c.CORSAllowedOrigins) + }) } diff --git a/backend/go.mod b/backend/go.mod index ba6304a..289cd5a 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -40,8 +40,10 @@ require ( github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f // indirect github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91 // indirect github.com/dop251/goja v0.0.0-20220124171016-cfb079cdc7b4 // indirect + github.com/felixge/httpsnoop v1.0.1 // indirect github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect github.com/golang/protobuf v1.5.2 // indirect + github.com/gorilla/handlers v1.5.1 // indirect github.com/jackc/chunkreader/v2 v2.0.1 // indirect github.com/jackc/pgio v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect diff --git a/backend/go.sum b/backend/go.sum index 30a759f..6d82757 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -151,6 +151,8 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.m github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ= +github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4= github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= @@ -232,6 +234,8 @@ github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= +github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4= +github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q= github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= diff --git a/backend/server/handler.go b/backend/server/handler.go index d028083..c8ed806 100644 --- a/backend/server/handler.go +++ b/backend/server/handler.go @@ -10,6 +10,7 @@ import ( "git.netflux.io/rob/clipper/filestore" "git.netflux.io/rob/clipper/media" "github.com/google/uuid" + "github.com/gorilla/handlers" "github.com/gorilla/mux" "github.com/gorilla/schema" "github.com/improbable-eng/grpc-web/go/grpcweb" @@ -50,7 +51,7 @@ func newHTTPHandler(grpcHandler *grpcweb.WrappedGrpcServer, mediaSetService Medi h. Methods("POST"). Path("/api/media_sets/{id}/clip"). - HandlerFunc(h.handleClip) + Handler(http.HandlerFunc(h.handleClip)) if c.FileStore == config.FileSystemStore { h. @@ -63,6 +64,8 @@ func newHTTPHandler(grpcHandler *grpcweb.WrappedGrpcServer, mediaSetService Medi Methods("GET"). Handler(assetsHandler) + h.Use(handlers.CORS(handlers.AllowedOrigins(c.CORSAllowedOrigins))) + return h } diff --git a/backend/server/handler_test.go b/backend/server/handler_test.go index ec4bdbd..fbcefd6 100644 --- a/backend/server/handler_test.go +++ b/backend/server/handler_test.go @@ -20,13 +20,14 @@ import ( func TestHandler(t *testing.T) { testCases := []struct { - name string - path, body, method, contentType string - config config.Config - wantStartFrame, wantEndFrame int64 - wantAudioFormat media.AudioFormat - wantStatus int - wantContentType, wantContentDisp, wantBody string + name string + path, body, method, contentType, origin string + config config.Config + wantStartFrame, wantEndFrame int64 + wantAudioFormat media.AudioFormat + wantStatus int + wantHeaders map[string]string + wantBody string }{ { name: "assets disabled, file system store disabled, GET /", @@ -111,6 +112,32 @@ func TestHandler(t *testing.T) { config: config.Config{FileStore: config.FileSystemStore, FileStoreHTTPBaseURL: mustParseURL(t, "/"), FileStoreHTTPRoot: "testdata/http/filestore", AssetsHTTPRoot: "testdata/http/assets"}, wantStatus: http.StatusNotFound, }, + { + name: "assets enabled, configured with custom Allowed-Origins header, origin does not match", + path: "/css/style.css", + method: http.MethodGet, + origin: "https://localhost:3000", + config: config.Config{ + FileStore: config.S3Store, + AssetsHTTPRoot: "testdata/http/assets", + CORSAllowedOrigins: []string{"https://www.example.com"}, + }, + wantHeaders: map[string]string{"access-control-allow-origin": ""}, + wantStatus: http.StatusOK, + }, + { + name: "assets enabled, configured with custom Allowed-Origins header, origin does match", + path: "/css/style.css", + method: http.MethodGet, + origin: "https://www.example.com", + config: config.Config{ + FileStore: config.S3Store, + AssetsHTTPRoot: "testdata/http/assets", + CORSAllowedOrigins: []string{"https://www.example.com"}, + }, + wantHeaders: map[string]string{"access-control-allow-origin": "https://www.example.com"}, + wantStatus: http.StatusOK, + }, { name: "POST /api/media_sets/:id/clip, NOK, no body", path: "/api/media_sets/05951a4d-584e-4056-9ae7-08b9e4cd355d/clip", @@ -147,10 +174,12 @@ func TestHandler(t *testing.T) { wantStartFrame: 0, wantEndFrame: 1024, wantAudioFormat: media.AudioFormatMP3, - wantContentType: "audio/mp3", - wantContentDisp: "attachment; filename=clip.mp3", - wantStatus: http.StatusOK, - wantBody: "an audio file", + wantHeaders: map[string]string{ + "content-type": "audio/mp3", + "content-disposition": "attachment; filename=clip.mp3", + }, + wantStatus: http.StatusOK, + wantBody: "an audio file", }, { name: "POST /api/media_sets/:id/clip, WAV, OK", @@ -162,10 +191,12 @@ func TestHandler(t *testing.T) { wantStartFrame: 4096, wantEndFrame: 8192, wantAudioFormat: media.AudioFormatWAV, - wantContentType: "audio/wav", - wantContentDisp: "attachment; filename=clip.wav", - wantStatus: http.StatusOK, - wantBody: "an audio file", + wantHeaders: map[string]string{ + "content-type": "audio/wav", + "content-disposition": "attachment; filename=clip.wav", + }, + wantStatus: http.StatusOK, + wantBody: "an audio file", }, } @@ -192,6 +223,9 @@ func TestHandler(t *testing.T) { body = strings.NewReader(tc.body) } req := httptest.NewRequest(tc.method, tc.path, body) + if tc.origin != "" { + req.Header.Add("origin", tc.origin) + } if tc.contentType != "" { req.Header.Add("content-type", tc.contentType) } @@ -206,11 +240,8 @@ func TestHandler(t *testing.T) { require.NoError(t, err) assert.Equal(t, tc.wantBody, string(body)) } - if tc.wantContentType != "" { - assert.Equal(t, tc.wantContentType, resp.Header.Get("content-type")) - } - if tc.wantContentDisp != "" { - assert.Equal(t, tc.wantContentDisp, resp.Header.Get("content-disposition")) + for k, v := range tc.wantHeaders { + assert.Equal(t, v, resp.Header.Get(k)) } }) } diff --git a/backend/server/server.go b/backend/server/server.go index 0cd1a60..a926063 100644 --- a/backend/server/server.go +++ b/backend/server/server.go @@ -99,6 +99,7 @@ func Start(options Options) error { mediaSetController := &mediaSetServiceController{mediaSetService: mediaSetService, logger: options.Logger.Sugar().Named("controller")} pbmediaset.RegisterMediaSetServiceServer(grpcServer, mediaSetController) + // TODO: implement CORS headers grpcHandler := grpcweb.WrapServer(grpcServer, grpcweb.WithOriginFunc(func(string) bool { return true })) httpHandler := newHTTPHandler(grpcHandler, mediaSetService, conf, options.Logger.Sugar().Named("httpHandler"))