Add CORS headers to HTTP handlers
continuous-integration/drone/push Build is passing Details

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

View File

@ -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
} }

View File

@ -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)
})
} }

View File

@ -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

View File

@ -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=

View File

@ -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
} }

View File

@ -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"))
} }
}) })
} }

View File

@ -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"))