HH-547: allow cross-origin widget requests (#126)

* HH-547: allow cross-origin widget requests

* fix(HH-547): align production preflight with wildcard CORS

---------

Co-authored-by: Rogee <rogee@ipao.vip>
This commit is contained in:
Rogee
2026-08-23 20:11:04 +08:00
committed by GitHub
co-authored by rogee
parent a2e4f9a1e8
commit eb83d241fe
13 changed files with 79 additions and 50 deletions
+7 -10
View File
@@ -24,7 +24,7 @@ type CORSConfig struct {
// Default CORS values for production.
var defaultCORSMethods = []string{"GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"}
var defaultCORSHeaders = []string{"Origin", "Content-Type", "Accept", "Authorization", "X-Account-ID", "access-token", "client", "uid", "token-type", "expiry"}
var defaultCORSHeaders = []string{"Origin", "Content-Type", "Accept", "Authorization", "X-Account-ID", "X-Auth-Token", "X-Widget-Token", "X-Identifier-Hash", "access-token", "client", "uid", "token-type", "expiry"}
var defaultCORSExposeHeaders = []string{"Content-Length", "access-token", "client", "uid", "token-type", "expiry"}
var defaultCORSMaxAge = 86400 // 24 hours
@@ -170,16 +170,13 @@ func extractHost(origin string) string {
return u.Hostname()
}
// CORSConfigFromAppConfig creates a middleware CORSConfig from the application config.
// CORSConfigFromAppConfig keeps method/header tuning while allowing every embed origin.
func CORSConfigFromAppConfig(cfg *config.Config) CORSConfig {
devMode := cfg.Server.Mode == "debug"
return CORSConfig{
AllowedOrigins: cfg.Server.CORS.AllowedOrigins,
AllowedMethods: cfg.Server.CORS.AllowedMethods,
AllowedHeaders: cfg.Server.CORS.AllowedHeaders,
ExposeHeaders: cfg.Server.CORS.ExposeHeaders,
AllowCredentials: cfg.Server.CORS.AllowCredentials,
MaxAge: cfg.Server.CORS.MaxAge,
DevMode: devMode,
AllowedMethods: cfg.Server.CORS.AllowedMethods,
AllowedHeaders: cfg.Server.CORS.AllowedHeaders,
ExposeHeaders: cfg.Server.CORS.ExposeHeaders,
MaxAge: cfg.Server.CORS.MaxAge,
DevMode: true,
}
}
+34 -7
View File
@@ -58,11 +58,38 @@ func TestCORS_DefaultAllowedHeadersIncludeChatwootAuthTokens(t *testing.T) {
router.ServeHTTP(w, req)
allowedHeaders := w.Header().Get("Access-Control-Allow-Headers")
for _, header := range []string{"access-token", "client", "uid", "token-type", "expiry"} {
for _, header := range []string{"access-token", "client", "uid", "token-type", "expiry", "X-Auth-Token", "X-Widget-Token", "X-Identifier-Hash"} {
assert.Contains(t, allowedHeaders, header)
}
}
func TestCORSConfigFromAppConfig_AllowsWidgetPreflightFromAnyProductionOrigin(t *testing.T) {
cfg := &config.Config{Server: config.ServerConfig{
Mode: "release",
CORS: config.CORSConfig{
AllowedOrigins: []string{"https://gochat.example.com"},
AllowCredentials: true,
},
}}
router := gin.New()
router.Use(CORS(CORSConfigFromAppConfig(cfg)))
router.POST("/api/v1/widget/conversations/toggle_typing", func(c *gin.Context) { c.Status(http.StatusOK) })
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodOptions, "/api/v1/widget/conversations/toggle_typing", nil)
req.Header.Set("Origin", "https://embedded.example.net")
req.Header.Set("Access-Control-Request-Method", http.MethodPost)
req.Header.Set("Access-Control-Request-Headers", "X-Auth-Token, X-Widget-Token, X-Identifier-Hash")
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusNoContent, w.Code)
assert.Equal(t, "*", w.Header().Get("Access-Control-Allow-Origin"))
for _, header := range []string{"X-Auth-Token", "X-Widget-Token", "X-Identifier-Hash"} {
assert.Contains(t, w.Header().Get("Access-Control-Allow-Headers"), header)
}
assert.Empty(t, w.Header().Get("Access-Control-Allow-Credentials"))
}
func TestCORS_DefaultExposeHeadersIncludeChatwootAuthTokens(t *testing.T) {
cfg := CORSConfig{DevMode: true}
router := gin.New()
@@ -316,8 +343,8 @@ func TestCORSConfigFromAppConfig_DebugMode(t *testing.T) {
mwCfg := CORSConfigFromAppConfig(cfg)
assert.True(t, mwCfg.DevMode)
assert.Equal(t, []string{"https://app.example.com"}, mwCfg.AllowedOrigins)
assert.True(t, mwCfg.AllowCredentials)
assert.Empty(t, mwCfg.AllowedOrigins)
assert.False(t, mwCfg.AllowCredentials)
assert.Equal(t, 3600, mwCfg.MaxAge)
}
@@ -337,12 +364,12 @@ func TestCORSConfigFromAppConfig_ProductionMode(t *testing.T) {
}
mwCfg := CORSConfigFromAppConfig(cfg)
assert.False(t, mwCfg.DevMode)
assert.Equal(t, []string{"https://app.example.com", "*.internal.com"}, mwCfg.AllowedOrigins)
assert.True(t, mwCfg.DevMode)
assert.Empty(t, mwCfg.AllowedOrigins)
assert.Equal(t, []string{"GET", "POST", "PUT"}, mwCfg.AllowedMethods)
assert.Equal(t, []string{"Authorization", "Content-Type"}, mwCfg.AllowedHeaders)
assert.Equal(t, []string{"X-Total-Count"}, mwCfg.ExposeHeaders)
assert.True(t, mwCfg.AllowCredentials)
assert.False(t, mwCfg.AllowCredentials)
assert.Equal(t, 7200, mwCfg.MaxAge)
}
@@ -355,7 +382,7 @@ func TestCORSConfigFromAppConfig_EmptyCORS(t *testing.T) {
}
mwCfg := CORSConfigFromAppConfig(cfg)
assert.False(t, mwCfg.DevMode)
assert.True(t, mwCfg.DevMode)
assert.Empty(t, mwCfg.AllowedOrigins)
assert.Empty(t, mwCfg.AllowedMethods) // defaults applied in CORS() middleware, not here
}
@@ -2018,7 +2018,7 @@ func TestCORSConfigFromAppConfig_Cov7(t *testing.T) {
cfg.Server.CORS.AllowedOrigins = []string{"https://app.com"}
result := CORSConfigFromAppConfig(cfg)
assert.True(t, result.DevMode)
assert.Equal(t, []string{"https://app.com"}, result.AllowedOrigins)
assert.Empty(t, result.AllowedOrigins)
}
// ===========================