Skip to content
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
1 change: 0 additions & 1 deletion api/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import "github.com/gin-gonic/gin"
func SetupCORS() gin.HandlerFunc {
return func(c *gin.Context) {
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi,

I removed Access-Control-Allow-Credentials. I believe it must NOT be set for now. The Fetch spec forbids pairing it with a wildcard origin, and browsers reject any credentialed request that receives both(we were initially setting both, but this PR will remove the one we don't use).

If credentials are ever needed, the wildcard above has to be replaced by a reflected, allowlisted origin first.

For now, we are a public API so I believe we should remove it for now.

c.Writer.Header().Set("Access-Control-Allow-Headers",
"Content-Type, Content-Length, Accept-Encoding, "+
"X-CSRF-Token, Authorization, accept, origin, "+
Expand Down
161 changes: 161 additions & 0 deletions api/middleware_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
// SPDX-License-Identifier: GPL-3.0-or-later

package api

import (
"net/http"
"net/http/httptest"
"testing"

"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func assertCORSHeaders(t *testing.T, headers http.Header) {
t.Helper()

assert.Equal(t, "*", headers.Get("Access-Control-Allow-Origin"))
assert.Empty(t, headers.Get("Access-Control-Allow-Credentials"),
"Allow-Credentials must not be set alongside a wildcard Allow-Origin")
assert.Equal(
t,
"Content-Type, Content-Length, Accept-Encoding, "+
"X-CSRF-Token, Authorization, accept, origin, "+
"Cache-Control, X-Requested-With",
headers.Get("Access-Control-Allow-Headers"),
)
assert.Equal(t, "POST, OPTIONS, GET, PUT, DELETE", headers.Get("Access-Control-Allow-Methods"))
}

func setupTestRouter(t *testing.T) *gin.Engine {
t.Helper()

previousMode := gin.Mode()
gin.SetMode(gin.TestMode)
t.Cleanup(func() { gin.SetMode(previousMode) })

router := gin.New()
router.Use(SetupCORS())
return router
}

func TestSetupCORS_NormalGET(t *testing.T) {
router := setupTestRouter(t)

handlerReached := false
router.GET("/test", func(c *gin.Context) {
handlerReached = true
c.String(http.StatusOK, "ok")
})

req := httptest.NewRequest(http.MethodGet, "/test", nil)
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)

require.Equal(t, http.StatusOK, rec.Code)
assert.True(t, handlerReached, "handler should be reached for normal GET")
assert.Equal(t, "ok", rec.Body.String())
assertCORSHeaders(t, rec.Header())
}

func TestSetupCORS_OPTIONS(t *testing.T) {
router := setupTestRouter(t)

handlerReached := false
router.OPTIONS("/test", func(c *gin.Context) {
handlerReached = true
c.String(http.StatusOK, "should not be reached")
})

req := httptest.NewRequest(http.MethodOptions, "/test", nil)
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)

require.Equal(t, http.StatusNoContent, rec.Code)
assert.False(t, handlerReached, "handler must not be reached on OPTIONS preflight")
assert.Empty(t, rec.Body.String())
assertCORSHeaders(t, rec.Header())
}

func TestSetupCORS_ErrorResponses(t *testing.T) {
tests := []struct {
name string
statusCode int
handler gin.HandlerFunc
}{
{
name: "400 Bad Request with AbortWithStatusJSON",
statusCode: http.StatusBadRequest,
handler: func(c *gin.Context) {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "bad request"})
},
},
{
name: "403 Forbidden with AbortWithStatus",
statusCode: http.StatusForbidden,
handler: func(c *gin.Context) {
c.AbortWithStatus(http.StatusForbidden)
},
},
{
name: "404 Not Found from handler",
statusCode: http.StatusNotFound,
handler: func(c *gin.Context) {
c.String(http.StatusNotFound, "not found")
},
},
{
name: "500 Internal Server Error with AbortWithStatus",
statusCode: http.StatusInternalServerError,
handler: func(c *gin.Context) {
c.AbortWithStatus(http.StatusInternalServerError)
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
router := setupTestRouter(t)
router.GET("/error", tt.handler)

req := httptest.NewRequest(http.MethodGet, "/error", nil)
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)

require.Equal(t, tt.statusCode, rec.Code)
assertCORSHeaders(t, rec.Header())
})
}
}

func TestSetupCORS_UnroutedRoute(t *testing.T) {
router := setupTestRouter(t)

req := httptest.NewRequest(http.MethodGet, "/nonexistent", nil)
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)

require.Equal(t, http.StatusNotFound, rec.Code)
assertCORSHeaders(t, rec.Header())
}

func TestSetupCORS_OtherMethods(t *testing.T) {
methods := []string{http.MethodPost, http.MethodPut, http.MethodDelete}

for _, method := range methods {
t.Run(method, func(t *testing.T) {
router := setupTestRouter(t)
router.Handle(method, "/test", func(c *gin.Context) {
c.Status(http.StatusOK)
})

req := httptest.NewRequest(method, "/test", nil)
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)

require.Equal(t, http.StatusOK, rec.Code)
assertCORSHeaders(t, rec.Header())
})
}
}
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ require (
github.com/glebarez/sqlite v1.11.0
github.com/go-sql-driver/mysql v1.9.3
github.com/spf13/viper v1.16.0
github.com/stretchr/testify v1.10.0
github.com/swaggo/files v1.0.1
github.com/swaggo/gin-swagger v1.6.0
github.com/swaggo/swag v1.16.5
Expand All @@ -23,6 +24,7 @@ require (
github.com/bytedance/sonic/loader v0.1.1 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
Expand Down Expand Up @@ -51,6 +53,7 @@ require (
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rogpeppe/go-internal v1.12.0 // indirect
github.com/spf13/afero v1.9.5 // indirect
Expand Down
Loading