Skip to content
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

MM-11160 Adding proper CORS support. #9152

Merged
merged 2 commits into from
Jul 26, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion Gopkg.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 10 additions & 3 deletions api4/apitestlib.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ func StopTestStore() {
}
}

func setupTestHelper(enterprise bool) *TestHelper {
func setupTestHelper(enterprise bool, updateConfig func(*model.Config)) *TestHelper {
permConfig, err := os.Open(utils.FindConfigFile("config.json"))
if err != nil {
panic(err)
Expand Down Expand Up @@ -115,6 +115,9 @@ func setupTestHelper(enterprise bool) *TestHelper {
if testStore != nil {
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.ListenAddress = ":0" })
}
if updateConfig != nil {
th.App.UpdateConfig(updateConfig)
}
serverErr := th.App.StartServer()
if serverErr != nil {
panic(serverErr)
Expand Down Expand Up @@ -161,11 +164,15 @@ func setupTestHelper(enterprise bool) *TestHelper {
}

func SetupEnterprise() *TestHelper {
return setupTestHelper(true)
return setupTestHelper(true, nil)
}

func Setup() *TestHelper {
return setupTestHelper(false)
return setupTestHelper(false, nil)
}

func SetupConfig(updateConfig func(cfg *model.Config)) *TestHelper {
return setupTestHelper(false, updateConfig)
}

func (me *TestHelper) TearDown() {
Expand Down
150 changes: 150 additions & 0 deletions api4/cors_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
package api4

import (
"fmt"
"net/http"
"testing"

"github.com/mattermost/mattermost-server/model"
"github.com/stretchr/testify/assert"
)

const (
acAllowOrigin = "Access-Control-Allow-Origin"
acExposeHeaders = "Access-Control-Expose-Headers"
acMaxAge = "Access-Control-Max-Age"
acAllowCredentials = "Access-Control-Allow-Credentials"
acAllowMethods = "Access-Control-Allow-Methods"
acAllowHeaders = "Access-Control-Allow-Headers"
)

func TestCORSRequestHandling(t *testing.T) {
for name, testcase := range map[string]struct {
AllowCorsFrom string
CorsExposedHeaders string
CorsAllowCredentials bool
ModifyRequest func(req *http.Request)
ExpectedAllowOrigin string
ExpectedExposeHeaders string
ExpectedAllowCredentials string
}{
"NoCORS": {
"",
"",
false,
func(req *http.Request) {
},
"",
"",
"",
},
"CORSEnabled": {
"http://somewhere.com",
"",
false,
func(req *http.Request) {
},
"",
"",
"",
},
"CORSEnabledStarOrigin": {
"*",
"",
false,
func(req *http.Request) {
req.Header.Set("Origin", "http://pre-release.mattermost.com")
},
"*",
"",
"",
},
"CORSEnabledStarNoOrigin": { // CORS spec requires this, not a bug.
"*",
"",
false,
func(req *http.Request) {
},
"",
"",
"",
},
"CORSEnabledMatching": {
"http://mattermost.com",
"",
false,
func(req *http.Request) {
req.Header.Set("Origin", "http://mattermost.com")
},
"http://mattermost.com",
"",
"",
},
"CORSEnabledMultiple": {
"http://spinmint.com http://mattermost.com",
"",
false,
func(req *http.Request) {
req.Header.Set("Origin", "http://mattermost.com")
},
"http://mattermost.com",
"",
"",
},
"CORSEnabledWithCredentials": {
"http://mattermost.com",
"",
true,
func(req *http.Request) {
req.Header.Set("Origin", "http://mattermost.com")
},
"http://mattermost.com",
"",
"true",
},
"CORSEnabledWithHeaders": {
"http://mattermost.com",
"x-my-special-header x-blueberry",
true,
func(req *http.Request) {
req.Header.Set("Origin", "http://mattermost.com")
},
"http://mattermost.com",
"X-My-Special-Header, X-Blueberry",
"true",
},
} {
t.Run(name, func(t *testing.T) {
th := SetupConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.AllowCorsFrom = testcase.AllowCorsFrom
*cfg.ServiceSettings.CorsExposedHeaders = testcase.CorsExposedHeaders
*cfg.ServiceSettings.CorsAllowCredentials = testcase.CorsAllowCredentials
})
defer th.TearDown()

port := th.App.Srv.ListenAddr.Port
host := fmt.Sprintf("http://localhost:%v", port)
url := fmt.Sprintf("%v/api/v4/system/ping", host)

req, err := http.NewRequest("GET", url, nil)
if err != nil {
t.Fatal(err)
}
testcase.ModifyRequest(req)

client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, http.StatusOK, resp.StatusCode)
assert.Equal(t, testcase.ExpectedAllowOrigin, resp.Header.Get(acAllowOrigin))
assert.Equal(t, testcase.ExpectedExposeHeaders, resp.Header.Get(acExposeHeaders))
assert.Equal(t, "", resp.Header.Get(acMaxAge))
assert.Equal(t, testcase.ExpectedAllowCredentials, resp.Header.Get(acAllowCredentials))
assert.Equal(t, "", resp.Header.Get(acAllowMethods))
assert.Equal(t, "", resp.Header.Get(acAllowHeaders))
})
}

}
55 changes: 24 additions & 31 deletions app/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"github.com/gorilla/handlers"
"github.com/gorilla/mux"
"github.com/pkg/errors"
"github.com/rs/cors"
"golang.org/x/crypto/acme/autocert"

"github.com/mattermost/mattermost-server/mlog"
Expand All @@ -44,7 +45,7 @@ type Server struct {
didFinishListen chan struct{}
}

var allowedMethods []string = []string{
var corsAllowedMethods []string = []string{
"POST",
"GET",
"OPTIONS",
Expand All @@ -61,35 +62,6 @@ func (rl *RecoveryLogger) Println(i ...interface{}) {
mlog.Error(fmt.Sprint(i))
}

type CorsWrapper struct {
config model.ConfigFunc
router *mux.Router
}

func (cw *CorsWrapper) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if allowed := *cw.config().ServiceSettings.AllowCorsFrom; allowed != "" {
if utils.CheckOrigin(r, allowed) {
w.Header().Set("Access-Control-Allow-Origin", r.Header.Get("Origin"))

if r.Method == "OPTIONS" {
w.Header().Set(
"Access-Control-Allow-Methods",
strings.Join(allowedMethods, ", "))

w.Header().Set(
"Access-Control-Allow-Headers",
r.Header.Get("Access-Control-Request-Headers"))
}
}
}

if r.Method == "OPTIONS" {
return
}

cw.router.ServeHTTP(w, r)
}

const TIME_TO_WAIT_FOR_CONNECTIONS_TO_CLOSE_ON_SERVER_SHUTDOWN = time.Second

// golang.org/x/crypto/acme/autocert/autocert.go
Expand All @@ -114,7 +86,28 @@ func stripPort(hostport string) string {
func (a *App) StartServer() error {
mlog.Info("Starting Server...")

var handler http.Handler = &CorsWrapper{a.Config, a.Srv.RootRouter}
var handler http.Handler = a.Srv.RootRouter
if allowedOrigins := *a.Config().ServiceSettings.AllowCorsFrom; allowedOrigins != "" {
exposedCorsHeaders := *a.Config().ServiceSettings.CorsExposedHeaders
allowCredentials := *a.Config().ServiceSettings.CorsAllowCredentials
debug := *a.Config().ServiceSettings.CorsDebug
corsWrapper := cors.New(cors.Options{
AllowedOrigins: strings.Fields(allowedOrigins),
AllowedMethods: corsAllowedMethods,
AllowedHeaders: []string{"*"},
ExposedHeaders: strings.Fields(exposedCorsHeaders),
MaxAge: 86400,
AllowCredentials: allowCredentials,
Debug: debug,
})

// If we have debugging of CORS turned on then forward messages to logs
if debug {
corsWrapper.Log = a.Log.StdLog(mlog.String("source", "cors"))
}

handler = corsWrapper.Handler(handler)
}

if *a.Config().RateLimitSettings.Enable {
mlog.Info("RateLimiter is enabled")
Expand Down
3 changes: 3 additions & 0 deletions config/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@
"EnforceMultifactorAuthentication": false,
"EnableUserAccessTokens": false,
"AllowCorsFrom": "",
"CorsExposedHeaders": "",
"CorsAllowCredentials": false,
"CorsDebug": false,
"AllowCookiesForSubdomains": false,
"SessionLengthWebInDays": 30,
"SessionLengthMobileInDays": 30,
Expand Down
15 changes: 15 additions & 0 deletions model/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,9 @@ type ServiceSettings struct {
EnforceMultifactorAuthentication *bool
EnableUserAccessTokens *bool
AllowCorsFrom *string
CorsExposedHeaders *string
CorsAllowCredentials *bool
CorsDebug *bool
AllowCookiesForSubdomains *bool
SessionLengthWebInDays *int
SessionLengthMobileInDays *int
Expand Down Expand Up @@ -413,6 +416,18 @@ func (s *ServiceSettings) SetDefaults() {
s.AllowCorsFrom = NewString(SERVICE_SETTINGS_DEFAULT_ALLOW_CORS_FROM)
}

if s.CorsExposedHeaders == nil {
s.CorsExposedHeaders = NewString("")
}

if s.CorsAllowCredentials == nil {
s.CorsAllowCredentials = NewBool(false)
}

if s.CorsDebug == nil {
s.CorsDebug = NewBool(false)
}

if s.AllowCookiesForSubdomains == nil {
s.AllowCookiesForSubdomains = NewBool(false)
}
Expand Down
8 changes: 8 additions & 0 deletions vendor/github.com/rs/cors/.travis.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 19 additions & 0 deletions vendor/github.com/rs/cors/LICENSE

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading