Add CORS headers to HTTP handlers

This commit is contained in:
Rob Watson 2022-01-26 19:27:57 +01:00
parent 698b97e904
commit cf90100c5f
7 changed files with 90 additions and 21 deletions

@ -41,6 +41,7 @@ type Config struct {
S3Bucket string S3Bucket string
AssetsHTTPRoot string AssetsHTTPRoot string
FFmpegWorkerPoolSize int FFmpegWorkerPoolSize int
CORSAllowedOrigins []string
} }
const Prefix = "CLIPPER_" 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{ return Config{
Environment: env, Environment: env,
BindAddr: bindAddr, BindAddr: bindAddr,
@ -156,5 +163,6 @@ func NewFromEnv() (Config, error) {
FileStoreHTTPRoot: fileStoreHTTPRoot, FileStoreHTTPRoot: fileStoreHTTPRoot,
FileStoreHTTPBaseURL: fileStoreHTTPBaseURL, FileStoreHTTPBaseURL: fileStoreHTTPBaseURL,
FFmpegWorkerPoolSize: ffmpegWorkerPoolSize, FFmpegWorkerPoolSize: ffmpegWorkerPoolSize,
CORSAllowedOrigins: corsAllowedOrigins,
}, nil }, nil
} }

@ -271,4 +271,24 @@ func TestNewFromEnv(t *testing.T) {
assert.Equal(t, "eu-west-1", c.AWSRegion) assert.Equal(t, "eu-west-1", c.AWSRegion)
assert.Equal(t, "bucket", c.S3Bucket) 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)
})
} }

@ -40,8 +40,10 @@ require (
github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f // indirect github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f // indirect
github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91 // indirect github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91 // indirect
github.com/dop251/goja v0.0.0-20220124171016-cfb079cdc7b4 // 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/go-sourcemap/sourcemap v2.1.3+incompatible // indirect
github.com/golang/protobuf v1.5.2 // 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/chunkreader/v2 v2.0.1 // indirect
github.com/jackc/pgio v1.0.0 // indirect github.com/jackc/pgio v1.0.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect

@ -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/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/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/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/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/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= 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/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/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/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.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.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=

@ -10,6 +10,7 @@ import (
"git.netflux.io/rob/clipper/filestore" "git.netflux.io/rob/clipper/filestore"
"git.netflux.io/rob/clipper/media" "git.netflux.io/rob/clipper/media"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/gorilla/handlers"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/gorilla/schema" "github.com/gorilla/schema"
"github.com/improbable-eng/grpc-web/go/grpcweb" "github.com/improbable-eng/grpc-web/go/grpcweb"
@ -50,7 +51,7 @@ func newHTTPHandler(grpcHandler *grpcweb.WrappedGrpcServer, mediaSetService Medi
h. h.
Methods("POST"). Methods("POST").
Path("/api/media_sets/{id}/clip"). Path("/api/media_sets/{id}/clip").
HandlerFunc(h.handleClip) Handler(http.HandlerFunc(h.handleClip))
if c.FileStore == config.FileSystemStore { if c.FileStore == config.FileSystemStore {
h. h.
@ -63,6 +64,8 @@ func newHTTPHandler(grpcHandler *grpcweb.WrappedGrpcServer, mediaSetService Medi
Methods("GET"). Methods("GET").
Handler(assetsHandler) Handler(assetsHandler)
h.Use(handlers.CORS(handlers.AllowedOrigins(c.CORSAllowedOrigins)))
return h return h
} }

@ -20,13 +20,14 @@ import (
func TestHandler(t *testing.T) { func TestHandler(t *testing.T) {
testCases := []struct { testCases := []struct {
name string name string
path, body, method, contentType string path, body, method, contentType, origin string
config config.Config config config.Config
wantStartFrame, wantEndFrame int64 wantStartFrame, wantEndFrame int64
wantAudioFormat media.AudioFormat wantAudioFormat media.AudioFormat
wantStatus int wantStatus int
wantContentType, wantContentDisp, wantBody string wantHeaders map[string]string
wantBody string
}{ }{
{ {
name: "assets disabled, file system store disabled, GET /", 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"}, config: config.Config{FileStore: config.FileSystemStore, FileStoreHTTPBaseURL: mustParseURL(t, "/"), FileStoreHTTPRoot: "testdata/http/filestore", AssetsHTTPRoot: "testdata/http/assets"},
wantStatus: http.StatusNotFound, 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", name: "POST /api/media_sets/:id/clip, NOK, no body",
path: "/api/media_sets/05951a4d-584e-4056-9ae7-08b9e4cd355d/clip", path: "/api/media_sets/05951a4d-584e-4056-9ae7-08b9e4cd355d/clip",
@ -147,10 +174,12 @@ func TestHandler(t *testing.T) {
wantStartFrame: 0, wantStartFrame: 0,
wantEndFrame: 1024, wantEndFrame: 1024,
wantAudioFormat: media.AudioFormatMP3, wantAudioFormat: media.AudioFormatMP3,
wantContentType: "audio/mp3", wantHeaders: map[string]string{
wantContentDisp: "attachment; filename=clip.mp3", "content-type": "audio/mp3",
wantStatus: http.StatusOK, "content-disposition": "attachment; filename=clip.mp3",
wantBody: "an audio file", },
wantStatus: http.StatusOK,
wantBody: "an audio file",
}, },
{ {
name: "POST /api/media_sets/:id/clip, WAV, OK", name: "POST /api/media_sets/:id/clip, WAV, OK",
@ -162,10 +191,12 @@ func TestHandler(t *testing.T) {
wantStartFrame: 4096, wantStartFrame: 4096,
wantEndFrame: 8192, wantEndFrame: 8192,
wantAudioFormat: media.AudioFormatWAV, wantAudioFormat: media.AudioFormatWAV,
wantContentType: "audio/wav", wantHeaders: map[string]string{
wantContentDisp: "attachment; filename=clip.wav", "content-type": "audio/wav",
wantStatus: http.StatusOK, "content-disposition": "attachment; filename=clip.wav",
wantBody: "an audio file", },
wantStatus: http.StatusOK,
wantBody: "an audio file",
}, },
} }
@ -192,6 +223,9 @@ func TestHandler(t *testing.T) {
body = strings.NewReader(tc.body) body = strings.NewReader(tc.body)
} }
req := httptest.NewRequest(tc.method, tc.path, body) req := httptest.NewRequest(tc.method, tc.path, body)
if tc.origin != "" {
req.Header.Add("origin", tc.origin)
}
if tc.contentType != "" { if tc.contentType != "" {
req.Header.Add("content-type", tc.contentType) req.Header.Add("content-type", tc.contentType)
} }
@ -206,11 +240,8 @@ func TestHandler(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, tc.wantBody, string(body)) assert.Equal(t, tc.wantBody, string(body))
} }
if tc.wantContentType != "" { for k, v := range tc.wantHeaders {
assert.Equal(t, tc.wantContentType, resp.Header.Get("content-type")) assert.Equal(t, v, resp.Header.Get(k))
}
if tc.wantContentDisp != "" {
assert.Equal(t, tc.wantContentDisp, resp.Header.Get("content-disposition"))
} }
}) })
} }

@ -99,6 +99,7 @@ func Start(options Options) error {
mediaSetController := &mediaSetServiceController{mediaSetService: mediaSetService, logger: options.Logger.Sugar().Named("controller")} mediaSetController := &mediaSetServiceController{mediaSetService: mediaSetService, logger: options.Logger.Sugar().Named("controller")}
pbmediaset.RegisterMediaSetServiceServer(grpcServer, mediaSetController) pbmediaset.RegisterMediaSetServiceServer(grpcServer, mediaSetController)
// TODO: implement CORS headers
grpcHandler := grpcweb.WrapServer(grpcServer, grpcweb.WithOriginFunc(func(string) bool { return true })) grpcHandler := grpcweb.WrapServer(grpcServer, grpcweb.WithOriginFunc(func(string) bool { return true }))
httpHandler := newHTTPHandler(grpcHandler, mediaSetService, conf, options.Logger.Sugar().Named("httpHandler")) httpHandler := newHTTPHandler(grpcHandler, mediaSetService, conf, options.Logger.Sugar().Named("httpHandler"))