From c5ec0cc41ecad26f443626e25f7248bc76e0c059 Mon Sep 17 00:00:00 2001 From: Alexander Bransby-Sharples Date: Thu, 16 Nov 2017 15:37:26 +0000 Subject: [PATCH] Add CORS support to fn api (#455) The Gin middleware is being used if one or more Origins are specified. Default setup for each Origin is as follows: - GET,POST, PUT, HEAD methods allowed - Credentials share disabled - Preflight requests cached for 12 hours Which are the defaults gin-contrib/cors comes with out of the box. Gin-cors will return a 403 if it gets a request with an Origin header that isn't on its' list. If no Origin header is specified then it will just return the servers response. Start fn with CORS enabled: `API_CORS="http://localhost:4000, http://localhost:3000" make run` --- api/server/server.go | 21 ++ glide.lock | 2 + glide.yaml | 7 +- vendor/github.com/gin-contrib/cors/.gitignore | 23 ++ .../github.com/gin-contrib/cors/.travis.yml | 22 ++ vendor/github.com/gin-contrib/cors/LICENSE | 21 ++ vendor/github.com/gin-contrib/cors/README.md | 92 ++++++ vendor/github.com/gin-contrib/cors/config.go | 83 +++++ vendor/github.com/gin-contrib/cors/cors.go | 102 ++++++ .../github.com/gin-contrib/cors/cors_test.go | 307 ++++++++++++++++++ .../gin-contrib/cors/examples/example.go | 29 ++ vendor/github.com/gin-contrib/cors/utils.go | 85 +++++ 12 files changed, 792 insertions(+), 2 deletions(-) create mode 100644 vendor/github.com/gin-contrib/cors/.gitignore create mode 100644 vendor/github.com/gin-contrib/cors/.travis.yml create mode 100644 vendor/github.com/gin-contrib/cors/LICENSE create mode 100644 vendor/github.com/gin-contrib/cors/README.md create mode 100644 vendor/github.com/gin-contrib/cors/config.go create mode 100644 vendor/github.com/gin-contrib/cors/cors.go create mode 100644 vendor/github.com/gin-contrib/cors/cors_test.go create mode 100644 vendor/github.com/gin-contrib/cors/examples/example.go create mode 100644 vendor/github.com/gin-contrib/cors/utils.go diff --git a/api/server/server.go b/api/server/server.go index 4ff4a82b1..39c88f33b 100644 --- a/api/server/server.go +++ b/api/server/server.go @@ -11,6 +11,7 @@ import ( "os" "path" "strconv" + "strings" "github.com/fnproject/fn/api" "github.com/fnproject/fn/api/agent" @@ -23,6 +24,7 @@ import ( "github.com/fnproject/fn/api/models" "github.com/fnproject/fn/api/mqs" "github.com/fnproject/fn/api/version" + "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" "github.com/opentracing/opentracing-go" "github.com/opentracing/opentracing-go/ext" @@ -38,6 +40,7 @@ const ( EnvLOGDBURL = "logstore_url" EnvPort = "port" // be careful, Gin expects this variable to be "port" EnvAPIURL = "api_url" + EnvAPICORS = "api_cors" EnvZipkinURL = "zipkin_url" ) @@ -75,6 +78,22 @@ func NewFromEnv(ctx context.Context, opts ...ServerOption) *Server { return New(ctx, ds, mq, logDB, opts...) } +func optionalCorsWrap(r *gin.Engine) { + // By default no CORS are allowed unless one + // or more Origins are defined by the API_CORS + // environment variable. + if len(viper.GetString(EnvAPICORS)) > 0 { + origins := strings.Split(strings.Replace(viper.GetString(EnvAPICORS), " ", "", -1), ",") + + corsConfig := cors.DefaultConfig() + corsConfig.AllowOrigins = origins + + logrus.Infof("CORS enabled for domains: %s", origins) + + r.Use(cors.New(corsConfig)) + } +} + // New creates a new Functions server with the passed in datastore, message queue and API URL func New(ctx context.Context, ds models.Datastore, mq models.MessageQueue, logDB models.LogStore, opts ...ServerOption) *Server { @@ -90,6 +109,8 @@ func New(ctx context.Context, ds models.Datastore, mq models.MessageQueue, logDB setMachineID() s.Router.Use(loggerWrap, traceWrap, panicWrap) + optionalCorsWrap(s.Router) + s.bindHandlers(ctx) for _, opt := range opts { diff --git a/glide.lock b/glide.lock index 05fff1fc5..521ae39ab 100644 --- a/glide.lock +++ b/glide.lock @@ -185,6 +185,8 @@ imports: subpackages: - internal - redis +- name: github.com/gin-contrib/cors + version: cf4846e6a636a76237a28d9286f163c132e841bc - name: github.com/gin-contrib/sse version: 22d885f9ecc78bf4ee5d72b937e4bbcdc58e8cae - name: github.com/gin-gonic/gin diff --git a/glide.yaml b/glide.yaml index 198baa639..ca8a989fb 100644 --- a/glide.yaml +++ b/glide.yaml @@ -2,8 +2,10 @@ package: github.com/fnproject/fn excludeDirs: - cli import: -- package: golang.org/x/crypto/pkcs12 +- package: golang.org/x/crypto version: master + subpackages: + - pkcs12 - package: github.com/fnproject/fn_go version: ^0.2.0 subpackages: @@ -71,6 +73,7 @@ import: - package: github.com/prometheus/common version: 2f17f4a9d485bf34b4bfaccc273805040e4f86c8 - package: github.com/prometheus/client_golang +- package: github.com/gin-contrib/cors + version: ~1.2.0 testImport: - package: github.com/patrickmn/go-cache - branch: master diff --git a/vendor/github.com/gin-contrib/cors/.gitignore b/vendor/github.com/gin-contrib/cors/.gitignore new file mode 100644 index 000000000..b4ecae3ad --- /dev/null +++ b/vendor/github.com/gin-contrib/cors/.gitignore @@ -0,0 +1,23 @@ +*.o +*.a +*.so + +_obj +_test + +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof + +coverage.out diff --git a/vendor/github.com/gin-contrib/cors/.travis.yml b/vendor/github.com/gin-contrib/cors/.travis.yml new file mode 100644 index 000000000..c39908c58 --- /dev/null +++ b/vendor/github.com/gin-contrib/cors/.travis.yml @@ -0,0 +1,22 @@ +language: go +sudo: false + +go: + - 1.6.x + - 1.7.x + - 1.8.x + - tip + +script: + - go test -v -covermode=atomic -coverprofile=coverage.out + +after_success: + - bash <(curl -s https://codecov.io/bash) + +notifications: + webhooks: + urls: + - https://webhooks.gitter.im/e/acc2c57482e94b44f557 + on_success: change + on_failure: always + on_start: false diff --git a/vendor/github.com/gin-contrib/cors/LICENSE b/vendor/github.com/gin-contrib/cors/LICENSE new file mode 100644 index 000000000..4e2cfb015 --- /dev/null +++ b/vendor/github.com/gin-contrib/cors/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2016 Gin-Gonic + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/gin-contrib/cors/README.md b/vendor/github.com/gin-contrib/cors/README.md new file mode 100644 index 000000000..5a8ac705c --- /dev/null +++ b/vendor/github.com/gin-contrib/cors/README.md @@ -0,0 +1,92 @@ +# CORS gin's middleware + +[![Build Status](https://travis-ci.org/gin-contrib/cors.svg)](https://travis-ci.org/gin-contrib/cors) +[![codecov](https://codecov.io/gh/gin-contrib/cors/branch/master/graph/badge.svg)](https://codecov.io/gh/gin-contrib/cors) +[![Go Report Card](https://goreportcard.com/badge/github.com/gin-contrib/cors)](https://goreportcard.com/report/github.com/gin-contrib/cors) +[![GoDoc](https://godoc.org/github.com/gin-contrib/cors?status.svg)](https://godoc.org/github.com/gin-contrib/cors) +[![Join the chat at https://gitter.im/gin-gonic/gin](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/gin-gonic/gin) + +Gin middleware/handler to enable CORS support. + +## Usage + +### Start using it + +Download and install it: + +```sh +$ go get gopkg.in/gin-contrib/cors.v1 +``` + +Import it in your code: + +```go +import "gopkg.in/gin-contrib/cors.v1" +``` + +### Canonical example: + +```go +package main + +import ( + "time" + + "gopkg.in/gin-contrib/cors.v1" + "github.com/gin-gonic/gin" +) + +func main() { + router := gin.Default() + // CORS for https://foo.com and https://github.com origins, allowing: + // - PUT and PATCH methods + // - Origin header + // - Credentials share + // - Preflight requests cached for 12 hours + router.Use(cors.New(cors.Config{ + AllowOrigins: []string{"https://foo.com"}, + AllowMethods: []string{"PUT", "PATCH"}, + AllowHeaders: []string{"Origin"}, + ExposeHeaders: []string{"Content-Length"}, + AllowCredentials: true, + AllowOriginFunc: func(origin string) bool { + return origin == "https://github.com" + }, + MaxAge: 12 * time.Hour, + })) + router.Run() +} +``` + +### Using DefaultConfig as start point + +```go +func main() { + router := gin.Default() + // - No origin allowed by default + // - GET,POST, PUT, HEAD methods + // - Credentials share disabled + // - Preflight requests cached for 12 hours + config := cors.DefaultConfig() + config.AllowOrigins = []string{"http://google.com"} + config.AddAllowOrigins("http://facebook.com") + // config.AllowOrigins == []string{"http://google.com", "http://facebook.com"} + + router.Use(cors.New(config)) + router.Run() +} +``` + +### Default() allows all origins + +```go +func main() { + router := gin.Default() + // same as + // config := cors.DefaultConfig() + // config.AllowAllOrigins = true + // router.Use(cors.New(config)) + router.Use(cors.Default()) + router.Run() +} +``` diff --git a/vendor/github.com/gin-contrib/cors/config.go b/vendor/github.com/gin-contrib/cors/config.go new file mode 100644 index 000000000..8d2c0d225 --- /dev/null +++ b/vendor/github.com/gin-contrib/cors/config.go @@ -0,0 +1,83 @@ +package cors + +import ( + "net/http" + + "github.com/gin-gonic/gin" +) + +type cors struct { + allowAllOrigins bool + allowCredentials bool + allowOriginFunc func(string) bool + allowOrigins []string + exposeHeaders []string + normalHeaders http.Header + preflightHeaders http.Header +} + +func newCors(config Config) *cors { + if err := config.Validate(); err != nil { + panic(err.Error()) + } + return &cors{ + allowOriginFunc: config.AllowOriginFunc, + allowAllOrigins: config.AllowAllOrigins, + allowCredentials: config.AllowCredentials, + allowOrigins: normalize(config.AllowOrigins), + normalHeaders: generateNormalHeaders(config), + preflightHeaders: generatePreflightHeaders(config), + } +} + +func (cors *cors) applyCors(c *gin.Context) { + origin := c.Request.Header.Get("Origin") + if len(origin) == 0 { + // request is not a CORS request + return + } + if !cors.validateOrigin(origin) { + c.AbortWithStatus(http.StatusForbidden) + return + } + + if c.Request.Method == "OPTIONS" { + cors.handlePreflight(c) + defer c.AbortWithStatus(200) + } else { + cors.handleNormal(c) + } + + if !cors.allowAllOrigins { + c.Header("Access-Control-Allow-Origin", origin) + } +} + +func (cors *cors) validateOrigin(origin string) bool { + if cors.allowAllOrigins { + return true + } + for _, value := range cors.allowOrigins { + if value == origin { + return true + } + } + if cors.allowOriginFunc != nil { + return cors.allowOriginFunc(origin) + } + return false +} + +func (cors *cors) handlePreflight(c *gin.Context) { + header := c.Writer.Header() + for key, value := range cors.preflightHeaders { + header[key] = value + } +} + +func (cors *cors) handleNormal(c *gin.Context) { + header := c.Writer.Header() + for key, value := range cors.normalHeaders { + header[key] = value + } +} diff --git a/vendor/github.com/gin-contrib/cors/cors.go b/vendor/github.com/gin-contrib/cors/cors.go new file mode 100644 index 000000000..16550f4be --- /dev/null +++ b/vendor/github.com/gin-contrib/cors/cors.go @@ -0,0 +1,102 @@ +package cors + +import ( + "errors" + "strings" + "time" + + "github.com/gin-gonic/gin" +) + +// Config represents all available options for the middleware. +type Config struct { + AllowAllOrigins bool + + // AllowedOrigins is a list of origins a cross-domain request can be executed from. + // If the special "*" value is present in the list, all origins will be allowed. + // Default value is ["*"] + AllowOrigins []string + + // AllowOriginFunc is a custom function to validate the origin. It take the origin + // as argument and returns true if allowed or false otherwise. If this option is + // set, the content of AllowedOrigins is ignored. + AllowOriginFunc func(origin string) bool + + // AllowedMethods is a list of methods the client is allowed to use with + // cross-domain requests. Default value is simple methods (GET and POST) + AllowMethods []string + + // AllowedHeaders is list of non simple headers the client is allowed to use with + // cross-domain requests. + // If the special "*" value is present in the list, all headers will be allowed. + // Default value is [] but "Origin" is always appended to the list. + AllowHeaders []string + + // AllowCredentials indicates whether the request can include user credentials like + // cookies, HTTP authentication or client side SSL certificates. + AllowCredentials bool + + // ExposedHeaders indicates which headers are safe to expose to the API of a CORS + // API specification + ExposeHeaders []string + + // MaxAge indicates how long (in seconds) the results of a preflight request + // can be cached + MaxAge time.Duration +} + +// AddAllowMethods is allowed to add custom methods +func (c *Config) AddAllowMethods(methods ...string) { + c.AllowMethods = append(c.AllowMethods, methods...) +} + +// AddAllowHeaders is allowed to add custom headers +func (c *Config) AddAllowHeaders(headers ...string) { + c.AllowHeaders = append(c.AllowHeaders, headers...) +} + +// AddExposeHeaders is allowed to add custom expose headers +func (c *Config) AddExposeHeaders(headers ...string) { + c.ExposeHeaders = append(c.ExposeHeaders, headers...) +} + +// Validate is check configuration of user defined. +func (c Config) Validate() error { + if c.AllowAllOrigins && (c.AllowOriginFunc != nil || len(c.AllowOrigins) > 0) { + return errors.New("conflict settings: all origins are allowed. AllowOriginFunc or AllowedOrigins is not needed") + } + if !c.AllowAllOrigins && c.AllowOriginFunc == nil && len(c.AllowOrigins) == 0 { + return errors.New("conflict settings: all origins disabled") + } + for _, origin := range c.AllowOrigins { + if !strings.HasPrefix(origin, "http://") && !strings.HasPrefix(origin, "https://") { + return errors.New("bad origin: origins must include http:// or https://") + } + } + return nil +} + +// DefaultConfig returns a generic default configuration mapped to localhost. +func DefaultConfig() Config { + return Config{ + AllowMethods: []string{"GET", "POST", "PUT", "HEAD"}, + AllowHeaders: []string{"Origin", "Content-Length", "Content-Type"}, + AllowCredentials: false, + MaxAge: 12 * time.Hour, + } +} + +// Default returns the location middleware with default configuration. +func Default() gin.HandlerFunc { + config := DefaultConfig() + config.AllowAllOrigins = true + return New(config) +} + +// New returns the location middleware with user-defined custom configuration. +func New(config Config) gin.HandlerFunc { + cors := newCors(config) + return func(c *gin.Context) { + cors.applyCors(c) + } +} diff --git a/vendor/github.com/gin-contrib/cors/cors_test.go b/vendor/github.com/gin-contrib/cors/cors_test.go new file mode 100644 index 000000000..045bb4fb6 --- /dev/null +++ b/vendor/github.com/gin-contrib/cors/cors_test.go @@ -0,0 +1,307 @@ +package cors + +import ( + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/assert" +) + +func init() { + gin.SetMode(gin.TestMode) +} + +func newTestRouter(config Config) *gin.Engine { + router := gin.New() + router.Use(New(config)) + router.GET("/", func(c *gin.Context) { + c.String(200, "get") + }) + router.POST("/", func(c *gin.Context) { + c.String(200, "post") + }) + router.PATCH("/", func(c *gin.Context) { + c.String(200, "patch") + }) + return router +} + +func performRequest(r http.Handler, method, origin string) *httptest.ResponseRecorder { + req, _ := http.NewRequest(method, "/", nil) + if len(origin) > 0 { + req.Header.Set("Origin", origin) + } + w := httptest.NewRecorder() + r.ServeHTTP(w, req) + return w +} + +func TestConfigAddAllow(t *testing.T) { + config := Config{} + config.AddAllowMethods("POST") + config.AddAllowMethods("GET", "PUT") + config.AddExposeHeaders() + + config.AddAllowHeaders("Some", " cool") + config.AddAllowHeaders("header") + config.AddExposeHeaders() + + config.AddExposeHeaders() + config.AddExposeHeaders("exposed", "header") + config.AddExposeHeaders("hey") + + assert.Equal(t, config.AllowMethods, []string{"POST", "GET", "PUT"}) + assert.Equal(t, config.AllowHeaders, []string{"Some", " cool", "header"}) + assert.Equal(t, config.ExposeHeaders, []string{"exposed", "header", "hey"}) + +} + +func TestBadConfig(t *testing.T) { + assert.Panics(t, func() { New(Config{}) }) + assert.Panics(t, func() { + New(Config{ + AllowAllOrigins: true, + AllowOrigins: []string{"http://google.com"}, + }) + }) + assert.Panics(t, func() { + New(Config{ + AllowAllOrigins: true, + AllowOriginFunc: func(origin string) bool { return false }, + }) + }) + assert.Panics(t, func() { + New(Config{ + AllowOrigins: []string{"google.com"}, + }) + }) +} + +func TestNormalize(t *testing.T) { + values := normalize([]string{ + "http-Access ", "Post", "POST", " poSt ", + "HTTP-Access", "", + }) + assert.Equal(t, values, []string{"http-access", "post", ""}) + + values = normalize(nil) + assert.Nil(t, values) + + values = normalize([]string{}) + assert.Equal(t, values, []string{}) +} + +func TestConvert(t *testing.T) { + methods := []string{"Get", "GET", "get"} + headers := []string{"X-CSRF-TOKEN", "X-CSRF-Token", "x-csrf-token"} + + assert.Equal(t, []string{"GET", "GET", "GET"}, convert(methods, strings.ToUpper)) + assert.Equal(t, []string{"X-Csrf-Token", "X-Csrf-Token", "X-Csrf-Token"}, convert(headers, http.CanonicalHeaderKey)) +} + +func TestGenerateNormalHeaders_AllowAllOrigins(t *testing.T) { + header := generateNormalHeaders(Config{ + AllowAllOrigins: false, + }) + assert.Equal(t, header.Get("Access-Control-Allow-Origin"), "") + assert.Equal(t, header.Get("Vary"), "Origin") + assert.Len(t, header, 1) + + header = generateNormalHeaders(Config{ + AllowAllOrigins: true, + }) + assert.Equal(t, header.Get("Access-Control-Allow-Origin"), "*") + assert.Equal(t, header.Get("Vary"), "") + assert.Len(t, header, 1) +} + +func TestGenerateNormalHeaders_AllowCredentials(t *testing.T) { + header := generateNormalHeaders(Config{ + AllowCredentials: true, + }) + assert.Equal(t, header.Get("Access-Control-Allow-Credentials"), "true") + assert.Equal(t, header.Get("Vary"), "Origin") + assert.Len(t, header, 2) +} + +func TestGenerateNormalHeaders_ExposedHeaders(t *testing.T) { + header := generateNormalHeaders(Config{ + ExposeHeaders: []string{"X-user", "xPassword"}, + }) + assert.Equal(t, header.Get("Access-Control-Expose-Headers"), "X-User,Xpassword") + assert.Equal(t, header.Get("Vary"), "Origin") + assert.Len(t, header, 2) +} + +func TestGeneratePreflightHeaders(t *testing.T) { + header := generatePreflightHeaders(Config{ + AllowAllOrigins: false, + }) + assert.Equal(t, header.Get("Access-Control-Allow-Origin"), "") + assert.Equal(t, header.Get("Vary"), "Origin") + assert.Len(t, header, 1) + + header = generateNormalHeaders(Config{ + AllowAllOrigins: true, + }) + assert.Equal(t, header.Get("Access-Control-Allow-Origin"), "*") + assert.Equal(t, header.Get("Vary"), "") + assert.Len(t, header, 1) +} + +func TestGeneratePreflightHeaders_AllowCredentials(t *testing.T) { + header := generatePreflightHeaders(Config{ + AllowCredentials: true, + }) + assert.Equal(t, header.Get("Access-Control-Allow-Credentials"), "true") + assert.Equal(t, header.Get("Vary"), "Origin") + assert.Len(t, header, 2) +} + +func TestGeneratePreflightHeaders_AllowedMethods(t *testing.T) { + header := generatePreflightHeaders(Config{ + AllowMethods: []string{"GET ", "post", "PUT", " put "}, + }) + assert.Equal(t, header.Get("Access-Control-Allow-Methods"), "GET,POST,PUT") + assert.Equal(t, header.Get("Vary"), "Origin") + assert.Len(t, header, 2) +} + +func TestGeneratePreflightHeaders_AllowedHeaders(t *testing.T) { + header := generatePreflightHeaders(Config{ + AllowHeaders: []string{"X-user", "Content-Type"}, + }) + assert.Equal(t, header.Get("Access-Control-Allow-Headers"), "X-User,Content-Type") + assert.Equal(t, header.Get("Vary"), "Origin") + assert.Len(t, header, 2) +} + +func TestGeneratePreflightHeaders_MaxAge(t *testing.T) { + header := generatePreflightHeaders(Config{ + MaxAge: 12 * time.Hour, + }) + assert.Equal(t, header.Get("Access-Control-Max-Age"), "43200") // 12*60*60 + assert.Equal(t, header.Get("Vary"), "Origin") + assert.Len(t, header, 2) +} + +func TestValidateOrigin(t *testing.T) { + cors := newCors(Config{ + AllowAllOrigins: true, + }) + assert.True(t, cors.validateOrigin("http://google.com")) + assert.True(t, cors.validateOrigin("https://google.com")) + assert.True(t, cors.validateOrigin("example.com")) + + cors = newCors(Config{ + AllowOrigins: []string{"https://google.com", "https://github.com"}, + AllowOriginFunc: func(origin string) bool { + return (origin == "http://news.ycombinator.com") + }, + }) + assert.False(t, cors.validateOrigin("http://google.com")) + assert.True(t, cors.validateOrigin("https://google.com")) + assert.True(t, cors.validateOrigin("https://github.com")) + assert.True(t, cors.validateOrigin("http://news.ycombinator.com")) + assert.False(t, cors.validateOrigin("http://example.com")) + assert.False(t, cors.validateOrigin("google.com")) +} + +func TestPassesAllowedOrigins(t *testing.T) { + router := newTestRouter(Config{ + AllowOrigins: []string{"http://google.com"}, + AllowMethods: []string{" GeT ", "get", "post", "PUT ", "Head", "POST"}, + AllowHeaders: []string{"Content-type", "timeStamp "}, + ExposeHeaders: []string{"Data", "x-User"}, + AllowCredentials: false, + MaxAge: 12 * time.Hour, + AllowOriginFunc: func(origin string) bool { + return origin == "http://github.com" + }, + }) + + // no CORS request, origin == "" + w := performRequest(router, "GET", "") + assert.Equal(t, "get", w.Body.String()) + assert.Empty(t, w.Header().Get("Access-Control-Allow-Origin")) + assert.Empty(t, w.Header().Get("Access-Control-Allow-Credentials")) + assert.Empty(t, w.Header().Get("Access-Control-Expose-Headers")) + + // allowed CORS request + w = performRequest(router, "GET", "http://google.com") + assert.Equal(t, "get", w.Body.String()) + assert.Equal(t, "http://google.com", w.Header().Get("Access-Control-Allow-Origin")) + assert.Equal(t, "", w.Header().Get("Access-Control-Allow-Credentials")) + assert.Equal(t, "Data,X-User", w.Header().Get("Access-Control-Expose-Headers")) + + w = performRequest(router, "GET", "http://github.com") + assert.Equal(t, "get", w.Body.String()) + assert.Equal(t, "http://github.com", w.Header().Get("Access-Control-Allow-Origin")) + assert.Equal(t, "", w.Header().Get("Access-Control-Allow-Credentials")) + assert.Equal(t, "Data,X-User", w.Header().Get("Access-Control-Expose-Headers")) + + // deny CORS request + w = performRequest(router, "GET", "https://google.com") + assert.Equal(t, 403, w.Code) + assert.Empty(t, w.Header().Get("Access-Control-Allow-Origin")) + assert.Empty(t, w.Header().Get("Access-Control-Allow-Credentials")) + assert.Empty(t, w.Header().Get("Access-Control-Expose-Headers")) + + // allowed CORS prefligh request + w = performRequest(router, "OPTIONS", "http://github.com") + assert.Equal(t, 200, w.Code) + assert.Equal(t, "http://github.com", w.Header().Get("Access-Control-Allow-Origin")) + assert.Equal(t, "", w.Header().Get("Access-Control-Allow-Credentials")) + assert.Equal(t, "GET,POST,PUT,HEAD", w.Header().Get("Access-Control-Allow-Methods")) + assert.Equal(t, "Content-Type,Timestamp", w.Header().Get("Access-Control-Allow-Headers")) + assert.Equal(t, "43200", w.Header().Get("Access-Control-Max-Age")) + + // deny CORS prefligh request + w = performRequest(router, "OPTIONS", "http://example.com") + assert.Equal(t, 403, w.Code) + assert.Empty(t, w.Header().Get("Access-Control-Allow-Origin")) + assert.Empty(t, w.Header().Get("Access-Control-Allow-Credentials")) + assert.Empty(t, w.Header().Get("Access-Control-Allow-Methods")) + assert.Empty(t, w.Header().Get("Access-Control-Allow-Headers")) + assert.Empty(t, w.Header().Get("Access-Control-Max-Age")) +} + +func TestPassesAllowedAllOrigins(t *testing.T) { + router := newTestRouter(Config{ + AllowAllOrigins: true, + AllowMethods: []string{" Patch ", "get", "post", "POST"}, + AllowHeaders: []string{"Content-type", " testheader "}, + ExposeHeaders: []string{"Data2", "x-User2"}, + AllowCredentials: false, + MaxAge: 10 * time.Hour, + }) + + // no CORS request, origin == "" + w := performRequest(router, "GET", "") + assert.Equal(t, "get", w.Body.String()) + assert.Empty(t, w.Header().Get("Access-Control-Allow-Origin")) + assert.Empty(t, w.Header().Get("Access-Control-Allow-Credentials")) + assert.Empty(t, w.Header().Get("Access-Control-Expose-Headers")) + assert.Empty(t, w.Header().Get("Access-Control-Allow-Credentials")) + + // allowed CORS request + w = performRequest(router, "POST", "example.com") + assert.Equal(t, "post", w.Body.String()) + assert.Equal(t, "*", w.Header().Get("Access-Control-Allow-Origin")) + assert.Equal(t, "Data2,X-User2", w.Header().Get("Access-Control-Expose-Headers")) + assert.Empty(t, w.Header().Get("Access-Control-Allow-Credentials")) + assert.Equal(t, "*", w.Header().Get("Access-Control-Allow-Origin")) + + // allowed CORS prefligh request + w = performRequest(router, "OPTIONS", "https://facebook.com") + assert.Equal(t, 200, w.Code) + assert.Equal(t, "*", w.Header().Get("Access-Control-Allow-Origin")) + assert.Equal(t, "PATCH,GET,POST", w.Header().Get("Access-Control-Allow-Methods")) + assert.Equal(t, "Content-Type,Testheader", w.Header().Get("Access-Control-Allow-Headers")) + assert.Equal(t, "36000", w.Header().Get("Access-Control-Max-Age")) + assert.Empty(t, w.Header().Get("Access-Control-Allow-Credentials")) +} diff --git a/vendor/github.com/gin-contrib/cors/examples/example.go b/vendor/github.com/gin-contrib/cors/examples/example.go new file mode 100644 index 000000000..e57303cfb --- /dev/null +++ b/vendor/github.com/gin-contrib/cors/examples/example.go @@ -0,0 +1,29 @@ +package main + +import ( + "time" + + "github.com/gin-contrib/cors" + "github.com/gin-gonic/gin" +) + +func main() { + router := gin.Default() + // CORS for https://foo.com and https://github.com origins, allowing: + // - PUT and PATCH methods + // - Origin header + // - Credentials share + // - Preflight requests cached for 12 hours + router.Use(cors.New(cors.Config{ + AllowOrigins: []string{"https://foo.com"}, + AllowMethods: []string{"PUT", "PATCH"}, + AllowHeaders: []string{"Origin"}, + ExposeHeaders: []string{"Content-Length"}, + AllowCredentials: true, + AllowOriginFunc: func(origin string) bool { + return origin == "https://github.com" + }, + MaxAge: 12 * time.Hour, + })) + router.Run() +} diff --git a/vendor/github.com/gin-contrib/cors/utils.go b/vendor/github.com/gin-contrib/cors/utils.go new file mode 100644 index 000000000..460ef1780 --- /dev/null +++ b/vendor/github.com/gin-contrib/cors/utils.go @@ -0,0 +1,85 @@ +package cors + +import ( + "net/http" + "strconv" + "strings" + "time" +) + +type converter func(string) string + +func generateNormalHeaders(c Config) http.Header { + headers := make(http.Header) + if c.AllowCredentials { + headers.Set("Access-Control-Allow-Credentials", "true") + } + if len(c.ExposeHeaders) > 0 { + exposeHeaders := convert(normalize(c.ExposeHeaders), http.CanonicalHeaderKey) + headers.Set("Access-Control-Expose-Headers", strings.Join(exposeHeaders, ",")) + } + if c.AllowAllOrigins { + headers.Set("Access-Control-Allow-Origin", "*") + } else { + headers.Set("Vary", "Origin") + } + return headers +} + +func generatePreflightHeaders(c Config) http.Header { + headers := make(http.Header) + if c.AllowCredentials { + headers.Set("Access-Control-Allow-Credentials", "true") + } + if len(c.AllowMethods) > 0 { + allowMethods := convert(normalize(c.AllowMethods), strings.ToUpper) + value := strings.Join(allowMethods, ",") + headers.Set("Access-Control-Allow-Methods", value) + } + if len(c.AllowHeaders) > 0 { + allowHeaders := convert(normalize(c.AllowHeaders), http.CanonicalHeaderKey) + value := strings.Join(allowHeaders, ",") + headers.Set("Access-Control-Allow-Headers", value) + } + if c.MaxAge > time.Duration(0) { + value := strconv.FormatInt(int64(c.MaxAge/time.Second), 10) + headers.Set("Access-Control-Max-Age", value) + } + if c.AllowAllOrigins { + headers.Set("Access-Control-Allow-Origin", "*") + } else { + // Always set Vary headers + // see https://github.com/rs/cors/issues/10, + // https://github.com/rs/cors/commit/dbdca4d95feaa7511a46e6f1efb3b3aa505bc43f#commitcomment-12352001 + + headers.Add("Vary", "Origin") + headers.Add("Vary", "Access-Control-Request-Method") + headers.Add("Vary", "Access-Control-Request-Headers") + } + return headers +} + +func normalize(values []string) []string { + if values == nil { + return nil + } + distinctMap := make(map[string]bool, len(values)) + normalized := make([]string, 0, len(values)) + for _, value := range values { + value = strings.TrimSpace(value) + value = strings.ToLower(value) + if _, seen := distinctMap[value]; !seen { + normalized = append(normalized, value) + distinctMap[value] = true + } + } + return normalized +} + +func convert(s []string, c converter) []string { + var out []string + for _, i := range s { + out = append(out, c(i)) + } + return out +}