Skip to content

feat(cors): Added cross origin support #221

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
May 5, 2020
10 changes: 10 additions & 0 deletions cmd/optimizely/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,15 @@ func assertAPIAuth(t *testing.T, actual config.ServiceAuthConfig) {
assert.Equal(t, 25*time.Second, actual.JwksUpdateInterval)
}

func assertAPICORS(t *testing.T, actual config.CORSConfig) {
assert.Equal(t, []string{"http://test1.com", "http://test2.com"}, actual.AllowedOrigins)
assert.Equal(t, []string{"POST", "GET", "OPTIONS"}, actual.AllowedMethods)
assert.Equal(t, []string{"Accept", "Authorization"}, actual.AllowedHeaders)
assert.Equal(t, []string{"Header1"}, actual.ExposedHeaders)
assert.Equal(t, true, actual.AllowedCredentials)
assert.Equal(t, 500, actual.MaxAge)
}

func assertWebhook(t *testing.T, actual config.WebhookConfig) {
assert.Equal(t, "3001", actual.Port)
assert.Equal(t, "secret-10000", actual.Projects[10000].Secret)
Expand Down Expand Up @@ -121,6 +130,7 @@ func TestViperYaml(t *testing.T) {
assertAdminAuth(t, actual.Admin.Auth)
assertAPI(t, actual.API)
assertAPIAuth(t, actual.API.Auth)
assertAPICORS(t, actual.API.CORS)
assertWebhook(t, actual.Webhook)
}

Expand Down
15 changes: 15 additions & 0 deletions cmd/testdata/default.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,21 @@ api:
port: "3000"
enableNotifications: true
enableOverrides: true
cors:
allowedOrigins:
- "http://test1.com"
- "http://test2.com"
allowedMethods:
- "POST"
- "GET"
- "OPTIONS"
allowedHeaders:
- "Accept"
- "Authorization"
exposedHeaders:
- "Header1"
allowedCredentials: true
maxAge: 500
auth:
ttl: 30m
hmacSecrets:
Expand Down
21 changes: 19 additions & 2 deletions config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,31 @@ api:
enableNotifications: false
## set to true to be able to override experiment bucketing. (recommended false in production)
enableOverrides: true

## for details check this out.
## https://github.com/go-chi/cors
# cors:
# ## If allowedOrigins is nil or empty, value is set to ["*"].
# allowedOrigins:
# - ["*"]
# ## If allowedMethods is nil or empty, value is set to (HEAD, GET and POST).
# allowedMethods:
# - "HEAD"
# - "GET"
# - "POST"
# - "PUT"
# - "OPTIONS"
# - "DELETE"
# ## Default value is [] but "Origin" is always appended to the list.
# allowedHeaders: []
# exposedHeaders: []
# allowedCredentials: false
# maxAge: 300
##
## admin service configuration
##
admin:
## http listener port
port: "8088"

##
## webhook service receives update notifications to your Optimizely project. Receipt of the webhook will
## trigger an immediate download of the datafile from the CDN
Expand Down
22 changes: 22 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,17 @@ func NewDefaultConfig() *AgentConfig {
JwksURL: "",
JwksUpdateInterval: 0,
},
CORS: CORSConfig{
// If AllowedOrigins is nil or empty, value is set to ["*"].
AllowedOrigins: nil,
// If AllowedMethods is nil or empty, value is set to (HEAD, GET and POST).
AllowedMethods: nil,
// Default value is [] but "Origin" is always appended to the list.
AllowedHeaders: []string{},
ExposedHeaders: []string{},
AllowedCredentials: false,
MaxAge: 300,
},
MaxConns: 0,
Port: "8080",
EnableNotifications: false,
Expand Down Expand Up @@ -121,12 +132,23 @@ type ServerConfig struct {
// APIConfig holds the REST API configuration
type APIConfig struct {
Auth ServiceAuthConfig `json:"-"`
CORS CORSConfig `json:"cors"`
MaxConns int `json:"maxConns"`
Port string `json:"port"`
EnableNotifications bool `json:"enableNotifications"`
EnableOverrides bool `json:"enableOverrides"`
}

// CORSConfig holds the CORS middleware configuration
type CORSConfig struct {
AllowedOrigins []string `json:"allowedOrigins"`
AllowedMethods []string `json:"allowedMethods"`
AllowedHeaders []string `json:"allowedHeaders"`
ExposedHeaders []string `json:"exposedHeaders"`
AllowedCredentials bool `json:"allowedCredentials"`
MaxAge int `json:"maxAge"`
}

// AdminConfig holds the configuration for the admin web interface
type AdminConfig struct {
Auth ServiceAuthConfig `json:"-"`
Expand Down
6 changes: 6 additions & 0 deletions config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,12 @@ func TestDefaultConfig(t *testing.T) {
assert.Equal(t, time.Duration(0), conf.API.Auth.JwksUpdateInterval)
assert.Equal(t, false, conf.API.EnableOverrides)
assert.Equal(t, false, conf.API.EnableNotifications)
assert.Equal(t, []string(nil), conf.API.CORS.AllowedOrigins)
assert.Equal(t, []string(nil), conf.API.CORS.AllowedMethods)
assert.Equal(t, make([]string, 0), conf.API.CORS.AllowedHeaders)
assert.Equal(t, make([]string, 0), conf.API.CORS.ExposedHeaders)
assert.Equal(t, false, conf.API.CORS.AllowedCredentials)
assert.Equal(t, 300, conf.API.CORS.MaxAge)

assert.Equal(t, "8085", conf.Webhook.Port)
assert.Empty(t, conf.Webhook.Projects)
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ require (
github.com/VividCortex/gohistogram v1.0.0 // indirect
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/go-chi/chi v4.0.2+incompatible
github.com/go-chi/cors v1.1.1
github.com/go-chi/render v1.0.1
github.com/go-kit/kit v0.9.0
github.com/google/uuid v1.1.1
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-chi/chi v4.0.2+incompatible h1:maB6vn6FqCxrpz4FqWdh4+lwpyZIQS7YEAUcHlgXVRs=
github.com/go-chi/chi v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
github.com/go-chi/cors v1.1.1 h1:eHuqxsIw89iXcWnWUN8R72JMibABJTN/4IOYI5WERvw=
github.com/go-chi/cors v1.1.1/go.mod h1:K2Yje0VW/SJzxiyMYu6iPQYa7hMjQX2i/F491VChg1I=
github.com/go-chi/render v1.0.1 h1:4/5tis2cKaNdnv9zFLfXzcquC9HbeZgCnxGnKrltBS8=
github.com/go-chi/render v1.0.1/go.mod h1:pq4Rr7HbnsdaeHagklXub+p6Wd16Af5l9koip1OvJns=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
Expand Down
7 changes: 3 additions & 4 deletions pkg/handlers/notification.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,12 @@ package handlers
import (
"encoding/json"
"fmt"
"net/http"
"strings"

"github.com/optimizely/agent/pkg/middleware"
"github.com/optimizely/go-sdk/pkg/notification"
"github.com/optimizely/go-sdk/pkg/registry"
"net/http"
"strings"
)

// A MessageChan is a channel of bytes
Expand Down Expand Up @@ -80,8 +81,6 @@ func NotificationEventSteamHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
// this should be settable via config
w.Header().Set("Access-Control-Allow-Origin", "*")

// Each connection registers its own message channel with the NotificationHandler's connections registry
messageChan := make(MessageChan)
Expand Down
19 changes: 18 additions & 1 deletion pkg/routers/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import (

"github.com/go-chi/chi"
chimw "github.com/go-chi/chi/middleware"
"github.com/go-chi/cors"
"github.com/go-chi/render"
)

Expand All @@ -47,6 +48,7 @@ type APIOptions struct {
nStreamHandler http.HandlerFunc
oAuthHandler http.HandlerFunc
oAuthMiddleware func(next http.Handler) http.Handler
corsHandler func(next http.Handler) http.Handler
}

func forbiddenHandler(message string) http.HandlerFunc {
Expand Down Expand Up @@ -81,6 +83,7 @@ func NewDefaultAPIRouter(optlyCache optimizely.Cache, conf config.APIConfig, met
}

mw := middleware.CachedOptlyMiddleware{Cache: optlyCache}
corsHandler := createCorsHandler(conf.CORS)

spec := &APIOptions{
maxConns: conf.MaxConns,
Expand All @@ -93,6 +96,7 @@ func NewDefaultAPIRouter(optlyCache optimizely.Cache, conf config.APIConfig, met
nStreamHandler: nStreamHandler,
oAuthHandler: authHandler.CreateAPIAccessToken,
oAuthMiddleware: authProvider.AuthorizeAPI,
corsHandler: corsHandler,
}

return NewAPIRouter(spec)
Expand Down Expand Up @@ -123,7 +127,7 @@ func WithAPIRouter(opt *APIOptions, r chi.Router) {
r.Use(render.SetContentType(render.ContentTypeJSON), middleware.SetRequestID)

r.Route("/v1", func(r chi.Router) {
r.Use(opt.sdkMiddleware)
r.Use(opt.corsHandler, opt.sdkMiddleware)
r.With(getConfigTimer, opt.oAuthMiddleware).Get("/config", opt.configHandler)
r.With(activateTimer, opt.oAuthMiddleware).Post("/activate", opt.activateHandler)
r.With(trackTimer, opt.oAuthMiddleware).Post("/track", opt.trackHandler)
Expand All @@ -141,3 +145,16 @@ func WithAPIRouter(opt *APIOptions, r chi.Router) {
staticServer := http.FileServer(statikFS)
r.Handle("/*", staticServer)
}

func createCorsHandler(c config.CORSConfig) func(next http.Handler) http.Handler {
options := cors.Options{
AllowedOrigins: c.AllowedOrigins,
AllowedMethods: c.AllowedMethods,
AllowedHeaders: c.AllowedHeaders,
ExposedHeaders: c.ExposedHeaders,
AllowCredentials: c.AllowedCredentials,
MaxAge: c.MaxAge,
}

return cors.Handler(options)
}
Loading