cors.go 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116
  1. package middleware
  2. import (
  3. "log"
  4. "net/http"
  5. "strings"
  6. "sync"
  7. "github.com/Wei-Shaw/sub2api/internal/config"
  8. "github.com/gin-gonic/gin"
  9. )
  10. var corsWarningOnce sync.Once
  11. // CORS 跨域中间件
  12. func CORS(cfg config.CORSConfig) gin.HandlerFunc {
  13. allowedOrigins := normalizeOrigins(cfg.AllowedOrigins)
  14. allowAll := false
  15. for _, origin := range allowedOrigins {
  16. if origin == "*" {
  17. allowAll = true
  18. break
  19. }
  20. }
  21. wildcardWithSpecific := allowAll && len(allowedOrigins) > 1
  22. if wildcardWithSpecific {
  23. allowedOrigins = []string{"*"}
  24. }
  25. allowCredentials := cfg.AllowCredentials
  26. corsWarningOnce.Do(func() {
  27. if len(allowedOrigins) == 0 {
  28. log.Println("Warning: CORS allowed_origins not configured; cross-origin requests will be rejected.")
  29. }
  30. if wildcardWithSpecific {
  31. log.Println("Warning: CORS allowed_origins includes '*'; wildcard will take precedence over explicit origins.")
  32. }
  33. if allowAll && allowCredentials {
  34. log.Println("Warning: CORS allowed_origins set to '*', disabling allow_credentials.")
  35. }
  36. })
  37. if allowAll && allowCredentials {
  38. allowCredentials = false
  39. }
  40. allowedSet := make(map[string]struct{}, len(allowedOrigins))
  41. for _, origin := range allowedOrigins {
  42. if origin == "" || origin == "*" {
  43. continue
  44. }
  45. allowedSet[origin] = struct{}{}
  46. }
  47. allowHeaders := []string{
  48. "Content-Type", "Content-Length", "Accept-Encoding", "X-CSRF-Token", "Authorization",
  49. "accept", "origin", "Cache-Control", "X-Requested-With", "X-API-Key", "X-Admin-UI-Request", "X-User-UI-Request",
  50. }
  51. // OpenAI Node SDK 会发送 x-stainless-* 请求头,需在 CORS 中显式放行。
  52. openAIProperties := []string{
  53. "lang", "package-version", "os", "arch", "retry-count", "runtime",
  54. "runtime-version", "async", "helper-method", "poll-helper", "custom-poll-interval", "timeout",
  55. }
  56. for _, prop := range openAIProperties {
  57. allowHeaders = append(allowHeaders, "x-stainless-"+prop)
  58. }
  59. allowHeadersValue := strings.Join(allowHeaders, ", ")
  60. return func(c *gin.Context) {
  61. origin := strings.TrimSpace(c.GetHeader("Origin"))
  62. originAllowed := allowAll
  63. if origin != "" && !allowAll {
  64. _, originAllowed = allowedSet[origin]
  65. }
  66. if originAllowed {
  67. if allowAll {
  68. c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
  69. } else if origin != "" {
  70. c.Writer.Header().Set("Access-Control-Allow-Origin", origin)
  71. c.Writer.Header().Add("Vary", "Origin")
  72. }
  73. if allowCredentials {
  74. c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
  75. }
  76. c.Writer.Header().Set("Access-Control-Allow-Headers", allowHeadersValue)
  77. c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE, PATCH")
  78. c.Writer.Header().Set("Access-Control-Expose-Headers", "ETag, Server-Timing")
  79. c.Writer.Header().Set("Access-Control-Max-Age", "86400")
  80. }
  81. // 处理预检请求
  82. if c.Request.Method == http.MethodOptions {
  83. if originAllowed {
  84. c.AbortWithStatus(http.StatusNoContent)
  85. } else {
  86. c.AbortWithStatus(http.StatusForbidden)
  87. }
  88. return
  89. }
  90. c.Next()
  91. }
  92. }
  93. func normalizeOrigins(values []string) []string {
  94. if len(values) == 0 {
  95. return nil
  96. }
  97. normalized := make([]string, 0, len(values))
  98. for _, value := range values {
  99. trimmed := strings.TrimSpace(value)
  100. if trimmed == "" {
  101. continue
  102. }
  103. normalized = append(normalized, trimmed)
  104. }
  105. return normalized
  106. }