feat: implement CORS support with configurable allowed origins

This commit is contained in:
mlogclub
2026-05-27 22:05:02 +08:00
parent 5bf4935a1f
commit 4d816182ee
6 changed files with 158 additions and 7 deletions
+28 -6
View File
@@ -29,7 +29,7 @@ func NewServer() (*gin.Engine, error) {
printBanner()
app := gin.New()
app.Use(corsMiddleware())
app.Use(corsMiddleware(cfg.Server.CORS.AllowedOrigins))
app.Use(gin.Recovery())
app.Use(requestLogMiddleware())
app.Use(maxBodySizeMiddleware(cfg.Storage.MaxRequestBodySizeBytes()))
@@ -60,18 +60,40 @@ func NewServer() (*gin.Engine, error) {
return app, nil
}
func corsMiddleware() gin.HandlerFunc {
func corsMiddleware(allowedOrigins []string) gin.HandlerFunc {
allowHeaders := "Origin, Content-Type, Accept, Authorization, X-Requested-With, X-Guest-Id, X-Channel-Id, X-External-Id, X-External-Name, X-Customer-Session-Token, X-Customer-Session-Expires-At"
exposeHeaders := "Content-Length, Content-Type, Authorization, X-Guest-Id, X-Channel-Id, X-External-Id, X-External-Name, X-Customer-Session-Token, X-Customer-Session-Expires-At"
allowMethods := "GET, POST, PUT, PATCH, DELETE, OPTIONS"
allowedOriginSet := make(map[string]struct{}, len(allowedOrigins))
for _, origin := range allowedOrigins {
origin = strings.TrimRight(strings.TrimSpace(origin), "/")
if origin == "" {
continue
}
allowedOriginSet[origin] = struct{}{}
}
return func(ctx *gin.Context) {
if isWebsocketUpgrade(ctx) {
ctx.Next()
return
}
ctx.Header("Access-Control-Allow-Origin", "*")
ctx.Header("Access-Control-Allow-Headers", allowHeaders)
ctx.Header("Access-Control-Expose-Headers", exposeHeaders)
ctx.Header("Access-Control-Max-Age", "600")
origin := strings.TrimRight(strings.TrimSpace(ctx.GetHeader("Origin")), "/")
if origin != "" {
ctx.Header("Vary", "Origin")
if _, ok := allowedOriginSet[origin]; !ok {
if ctx.Request.Method == http.MethodOptions {
ctx.AbortWithStatus(http.StatusForbidden)
return
}
ctx.Next()
return
}
ctx.Header("Access-Control-Allow-Origin", origin)
ctx.Header("Access-Control-Allow-Methods", allowMethods)
ctx.Header("Access-Control-Allow-Headers", allowHeaders)
ctx.Header("Access-Control-Expose-Headers", exposeHeaders)
ctx.Header("Access-Control-Max-Age", "600")
}
if ctx.Request.Method == http.MethodOptions {
ctx.AbortWithStatus(http.StatusNoContent)
return
+74
View File
@@ -85,3 +85,77 @@ func TestNewServerSeparatesAPIStaticAndSPA(t *testing.T) {
}
}
}
func TestNewServerAllowsConfiguredCORSOrigin(t *testing.T) {
config.SetCurrent(&config.Config{
Server: config.ServerConfig{
CORS: config.CORSConfig{
AllowedOrigins: []string{"https://console.example.com"},
},
},
Storage: config.StorageConfig{
Local: config.LocalStorageConfig{
Root: "storage",
BaseURL: "/storage",
},
},
})
app, err := NewServer()
if err != nil {
t.Fatalf("NewServer() error = %v", err)
}
rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodOptions, "/api/auth/login", nil)
req.Header.Set("Origin", "https://console.example.com")
req.Header.Set("Access-Control-Request-Method", http.MethodPost)
app.ServeHTTP(rec, req)
if rec.Code != http.StatusNoContent {
t.Fatalf("status=%d want %d", rec.Code, http.StatusNoContent)
}
if got := rec.Header().Get("Access-Control-Allow-Origin"); got != "https://console.example.com" {
t.Fatalf("Access-Control-Allow-Origin=%q want %q", got, "https://console.example.com")
}
if got := rec.Header().Get("Access-Control-Allow-Methods"); !strings.Contains(got, http.MethodPost) {
t.Fatalf("Access-Control-Allow-Methods=%q should contain %q", got, http.MethodPost)
}
if got := rec.Header().Get("Vary"); got != "Origin" {
t.Fatalf("Vary=%q want %q", got, "Origin")
}
}
func TestNewServerRejectsUnconfiguredCORSOrigin(t *testing.T) {
config.SetCurrent(&config.Config{
Server: config.ServerConfig{
CORS: config.CORSConfig{
AllowedOrigins: []string{"https://console.example.com"},
},
},
Storage: config.StorageConfig{
Local: config.LocalStorageConfig{
Root: "storage",
BaseURL: "/storage",
},
},
})
app, err := NewServer()
if err != nil {
t.Fatalf("NewServer() error = %v", err)
}
rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodOptions, "/api/auth/login", nil)
req.Header.Set("Origin", "https://evil.example.com")
req.Header.Set("Access-Control-Request-Method", http.MethodPost)
app.ServeHTTP(rec, req)
if rec.Code != http.StatusForbidden {
t.Fatalf("status=%d want %d", rec.Code, http.StatusForbidden)
}
if got := rec.Header().Get("Access-Control-Allow-Origin"); got != "" {
t.Fatalf("Access-Control-Allow-Origin=%q want empty", got)
}
}