api_key_auth_google.go 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  1. package middleware
  2. import (
  3. "errors"
  4. "fmt"
  5. "strings"
  6. "github.com/Wei-Shaw/sub2api/internal/config"
  7. "github.com/Wei-Shaw/sub2api/internal/pkg/googleapi"
  8. "github.com/Wei-Shaw/sub2api/internal/pkg/ip"
  9. "github.com/Wei-Shaw/sub2api/internal/service"
  10. "github.com/gin-gonic/gin"
  11. )
  12. // APIKeyAuthGoogle is a Google-style error wrapper for API key auth.
  13. func APIKeyAuthGoogle(apiKeyService *service.APIKeyService, cfg *config.Config) gin.HandlerFunc {
  14. return APIKeyAuthWithSubscriptionGoogle(apiKeyService, nil, cfg)
  15. }
  16. // APIKeyAuthWithSubscriptionGoogle behaves like ApiKeyAuthWithSubscription but returns Google-style errors:
  17. // {"error":{"code":401,"message":"...","status":"UNAUTHENTICATED"}}
  18. //
  19. // It is intended for Gemini native endpoints (/v1beta) to match Gemini SDK expectations.
  20. func APIKeyAuthWithSubscriptionGoogle(apiKeyService *service.APIKeyService, subscriptionService *service.SubscriptionService, cfg *config.Config) gin.HandlerFunc {
  21. return func(c *gin.Context) {
  22. if rejectInvalidAuthAbuse(c, apiKeyService) {
  23. abortWithGoogleError(c, 429, "Too many invalid authentication attempts; retry later")
  24. return
  25. }
  26. if apiKeyHeadersTooLarge(c) {
  27. recordInvalidAuthFailure(c, apiKeyService)
  28. MarkIngressRejected(c, IngressRejectInvalidAPIKey)
  29. abortWithGoogleError(c, 401, "Invalid API key")
  30. return
  31. }
  32. if v := strings.TrimSpace(c.Query("api_key")); v != "" {
  33. recordInvalidAuthFailure(c, apiKeyService)
  34. MarkIngressRejected(c, IngressRejectQueryAPIKeyDeprecated)
  35. abortWithGoogleError(c, 400, "Query parameter api_key is deprecated. Use Authorization header or key instead.")
  36. return
  37. }
  38. apiKeyString := extractAPIKeyForGoogle(c)
  39. if apiKeyString == "" {
  40. recordInvalidAuthFailure(c, apiKeyService)
  41. if hasAPIKeyCredentialInput(c) {
  42. MarkIngressRejected(c, IngressRejectInvalidAPIKey)
  43. } else {
  44. MarkIngressRejected(c, IngressRejectAPIKeyRequired)
  45. }
  46. abortWithGoogleError(c, 401, "API key is required")
  47. return
  48. }
  49. if len(apiKeyString) > service.MaxAPIKeyCredentialBytes {
  50. recordInvalidAuthFailure(c, apiKeyService)
  51. MarkIngressRejected(c, IngressRejectInvalidAPIKey)
  52. abortWithGoogleError(c, 401, "Invalid API key")
  53. return
  54. }
  55. apiKey, err := apiKeyService.GetByKey(c.Request.Context(), apiKeyString)
  56. if err != nil {
  57. if errors.Is(err, service.ErrAPIKeyNotFound) {
  58. recordInvalidAuthFailure(c, apiKeyService)
  59. MarkIngressRejected(c, IngressRejectInvalidAPIKey)
  60. abortWithGoogleError(c, 401, "Invalid API key")
  61. return
  62. }
  63. if errors.Is(err, service.ErrAPIKeyAuthOverloaded) {
  64. MarkIngressRejected(c, IngressRejectAPIKeyAuthOverloaded)
  65. abortWithGoogleError(c, 503, "API key authentication is temporarily unavailable")
  66. return
  67. }
  68. abortWithGoogleError(c, 500, "Failed to validate API key")
  69. return
  70. }
  71. // 同 api_key_auth.go:早退中断前也写入 Ops 回退 key,便于错误日志展示
  72. // user/group/platform。
  73. SetOpsFallbackAPIKey(c, apiKey)
  74. // disabled / 未知状态 → 无条件拦截(expired 和 quota_exhausted 留给计费阶段,
  75. // 与主中间件 api_key_auth.go 保持一致)。
  76. if !apiKey.IsActive() &&
  77. apiKey.Status != service.StatusAPIKeyExpired &&
  78. apiKey.Status != service.StatusAPIKeyQuotaExhausted {
  79. MarkIngressRejected(c, IngressRejectAPIKeyDisabled)
  80. abortWithGoogleError(c, 401, "API key is disabled")
  81. return
  82. }
  83. // 检查 IP 限制(白名单/黑名单)。与主中间件保持一致,避免 Gemini 端点绕过 Key 的 IP ACL。
  84. if len(apiKey.IPWhitelist) > 0 || len(apiKey.IPBlacklist) > 0 {
  85. clientIP := ip.GetSecurityClientIP(c, cfg.TrustForwardedIPForAPIKeyACL())
  86. allowed, _ := ip.CheckIPRestrictionWithCompiledRules(clientIP, apiKey.CompiledIPWhitelist, apiKey.CompiledIPBlacklist)
  87. if !allowed {
  88. if clientIP == "" {
  89. clientIP = "unknown"
  90. }
  91. service.MarkOpsClientBusinessLimited(c, service.OpsClientBusinessLimitedReasonIPRestriction)
  92. MarkIngressRejected(c, IngressRejectIPRestricted)
  93. abortWithGoogleError(c, 403, fmt.Sprintf("Access denied. Your IP is %s", clientIP))
  94. return
  95. }
  96. }
  97. if apiKey.User == nil {
  98. abortWithGoogleError(c, 401, "User associated with API key not found")
  99. return
  100. }
  101. if !apiKey.User.IsActive() {
  102. MarkIngressRejected(c, IngressRejectUserInactive)
  103. abortWithGoogleError(c, 401, "User account is not active")
  104. return
  105. }
  106. if code, message, ok := validateAPIKeyGroupAvailable(apiKey); !ok {
  107. service.MarkOpsClientBusinessLimited(c, service.OpsClientBusinessLimitedReasonAPIKeyGroupUnavailable)
  108. if code == "GROUP_DELETED" {
  109. MarkIngressRejected(c, IngressRejectGroupDeleted)
  110. } else {
  111. MarkIngressRejected(c, IngressRejectGroupDisabled)
  112. }
  113. abortWithGoogleError(c, 403, message)
  114. return
  115. }
  116. // 专属分组授权校验:用户对该专属分组的授权被撤销后应拒绝(与主中间件一致,防止越权)。
  117. if !validateAPIKeyGroupAllowed(apiKey) {
  118. service.MarkOpsClientBusinessLimited(c, service.OpsClientBusinessLimitedReasonAPIKeyGroupUnavailable)
  119. MarkIngressRejected(c, IngressRejectGroupNotAllowed)
  120. abortWithGoogleError(c, 403, "API Key 所属专属分组不再允许当前用户使用")
  121. return
  122. }
  123. // 简易模式:跳过余额和订阅检查
  124. if cfg.RunMode == config.RunModeSimple {
  125. c.Set(string(ContextKeyAPIKey), apiKey)
  126. c.Set(string(ContextKeyUser), AuthSubject{
  127. UserID: apiKey.User.ID,
  128. Concurrency: apiKey.User.Concurrency,
  129. })
  130. c.Set(string(ContextKeyUserRole), apiKey.User.Role)
  131. setGroupContext(c, apiKey.Group)
  132. _ = apiKeyService.TouchLastUsed(c.Request.Context(), apiKey.ID)
  133. c.Next()
  134. return
  135. }
  136. // Key 状态检查(状态字段可能因后台异步刷新而滞后,故显式拦截)。
  137. switch apiKey.Status {
  138. case service.StatusAPIKeyQuotaExhausted:
  139. abortWithGoogleError(c, 429, "API key 额度已用完")
  140. return
  141. case service.StatusAPIKeyExpired:
  142. abortWithGoogleError(c, 403, "API key 已过期")
  143. return
  144. }
  145. // 运行时过期/配额检查(即使状态是 active,也要检查时间和用量,与主中间件一致)。
  146. if apiKey.IsExpired() {
  147. abortWithGoogleError(c, 403, "API key 已过期")
  148. return
  149. }
  150. if apiKey.IsQuotaExhausted() {
  151. abortWithGoogleError(c, 429, "API key 额度已用完")
  152. return
  153. }
  154. isSubscriptionType := apiKey.Group != nil && apiKey.Group.IsSubscriptionType()
  155. if isSubscriptionType && subscriptionService != nil {
  156. subscription, err := subscriptionService.GetActiveSubscription(
  157. c.Request.Context(),
  158. apiKey.User.ID,
  159. apiKey.Group.ID,
  160. )
  161. if err != nil {
  162. abortWithGoogleError(c, 403, "No active subscription found for this group")
  163. return
  164. }
  165. needsMaintenance, err := subscriptionService.ValidateAndCheckLimits(subscription, apiKey.Group)
  166. if needsMaintenance {
  167. refreshed, maintenanceErr := subscriptionService.EnsureWindowMaintenance(c.Request.Context(), subscription)
  168. if maintenanceErr != nil {
  169. abortWithGoogleError(c, 500, "Failed to maintain subscription usage windows")
  170. return
  171. }
  172. subscription = refreshed
  173. _, err = subscriptionService.ValidateAndCheckLimits(subscription, apiKey.Group)
  174. }
  175. if err != nil {
  176. status := 403
  177. if errors.Is(err, service.ErrDailyLimitExceeded) ||
  178. errors.Is(err, service.ErrWeeklyLimitExceeded) ||
  179. errors.Is(err, service.ErrMonthlyLimitExceeded) {
  180. status = 429
  181. }
  182. abortWithGoogleError(c, status, err.Error())
  183. return
  184. }
  185. c.Set(string(ContextKeySubscription), subscription)
  186. } else {
  187. if apiKeyBalanceBelowAuthThreshold(apiKey.User.Balance, cfg) {
  188. abortWithGoogleError(c, 403, "Insufficient account balance")
  189. return
  190. }
  191. }
  192. c.Set(string(ContextKeyAPIKey), apiKey)
  193. c.Set(string(ContextKeyUser), AuthSubject{
  194. UserID: apiKey.User.ID,
  195. Concurrency: apiKey.User.Concurrency,
  196. })
  197. c.Set(string(ContextKeyUserRole), apiKey.User.Role)
  198. setGroupContext(c, apiKey.Group)
  199. _ = apiKeyService.TouchLastUsed(c.Request.Context(), apiKey.ID)
  200. c.Next()
  201. }
  202. }
  203. // extractAPIKeyForGoogle extracts API key for Google/Gemini endpoints.
  204. // Priority: x-goog-api-key > Authorization: Bearer > x-api-key > query key
  205. // This allows OpenClaw and other clients using Bearer auth to work with Gemini endpoints.
  206. func extractAPIKeyForGoogle(c *gin.Context) string {
  207. // 1) preferred: Gemini native header
  208. if k := strings.TrimSpace(c.GetHeader("x-goog-api-key")); k != "" {
  209. return k
  210. }
  211. // 2) fallback: Authorization: Bearer <key>
  212. auth := strings.TrimSpace(c.GetHeader("Authorization"))
  213. if auth != "" {
  214. parts := strings.SplitN(auth, " ", 2)
  215. if len(parts) == 2 && strings.EqualFold(parts[0], "Bearer") {
  216. if k := strings.TrimSpace(parts[1]); k != "" {
  217. return k
  218. }
  219. }
  220. }
  221. // 3) x-api-key header (backward compatibility)
  222. if k := strings.TrimSpace(c.GetHeader("x-api-key")); k != "" {
  223. return k
  224. }
  225. // 4) query parameter key (for specific paths)
  226. if allowGoogleQueryKey(c.Request.URL.Path) {
  227. if v := strings.TrimSpace(c.Query("key")); v != "" {
  228. return v
  229. }
  230. }
  231. return ""
  232. }
  233. func allowGoogleQueryKey(path string) bool {
  234. return strings.HasPrefix(path, "/v1beta") || strings.HasPrefix(path, "/antigravity/v1beta")
  235. }
  236. func abortWithGoogleError(c *gin.Context, status int, message string) {
  237. c.JSON(status, gin.H{
  238. "error": gin.H{
  239. "code": status,
  240. "message": message,
  241. "status": googleapi.HTTPStatusToGoogleStatus(status),
  242. },
  243. })
  244. c.Abort()
  245. }