mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-14 12:02:27 +08:00
Merge pull request #7674 from william-drakemond/fix/antigravity-sanitize-upstream-error
fix(antigravity): 返回客户端前脱敏上游错误体,避免泄露账号池身份
This commit is contained in:
@@ -194,7 +194,6 @@ func (s *AntigravityGatewayService) ForwardGemini(ctx context.Context, c *gin.Co
|
||||
// 处理错误响应
|
||||
if resp.StatusCode >= 400 {
|
||||
respBody := s.readUpstreamErrorBody(resp)
|
||||
contentType := resp.Header.Get("Content-Type")
|
||||
// 尽早关闭原始响应体,释放连接;后续逻辑仍可能需要读取 body,因此用内存副本重新包装。
|
||||
_ = resp.Body.Close()
|
||||
resp.Body = io.NopCloser(bytes.NewReader(respBody))
|
||||
@@ -301,7 +300,6 @@ func (s *AntigravityGatewayService) ForwardGemini(ctx context.Context, c *gin.Co
|
||||
Header: retryResp.Header.Clone(),
|
||||
Body: io.NopCloser(bytes.NewReader(retryRespBody)),
|
||||
}
|
||||
contentType = resp.Header.Get("Content-Type")
|
||||
}
|
||||
} else {
|
||||
if switchErr, ok := IsAntigravityAccountSwitchError(retryErr); ok {
|
||||
@@ -393,9 +391,6 @@ func (s *AntigravityGatewayService) ForwardGemini(ctx context.Context, c *gin.Co
|
||||
})
|
||||
return nil, &UpstreamFailoverError{StatusCode: resp.StatusCode, ResponseBody: unwrappedForOps}
|
||||
}
|
||||
if contentType == "" {
|
||||
contentType = "application/json"
|
||||
}
|
||||
appendOpsUpstreamError(c, OpsUpstreamErrorEvent{
|
||||
ProxyID: opsUpstreamProxyID(account),
|
||||
ProxyName: opsUpstreamProxyName(account),
|
||||
@@ -410,7 +405,8 @@ func (s *AntigravityGatewayService) ForwardGemini(ctx context.Context, c *gin.Co
|
||||
})
|
||||
logger.LegacyPrintf("service.antigravity_gateway", "[antigravity-Forward] upstream error status=%d body=%s", resp.StatusCode, truncateForLog(unwrappedForOps, 500))
|
||||
MarkResponseCommitted(c)
|
||||
c.Data(resp.StatusCode, contentType, unwrappedForOps)
|
||||
// 原始上游错误体仅保留在 ops 日志中;返回客户端的错误体需脱敏,避免泄露账号池身份(项目号/服务账号等)
|
||||
c.Data(resp.StatusCode, "application/json", buildAntigravityClientErrorBody(resp.StatusCode, unwrappedForOps))
|
||||
return nil, fmt.Errorf("antigravity upstream error: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
antigravityProjectRefRegex = regexp.MustCompile(`(?i)\bprojects/[a-z0-9][a-z0-9._:-]*`)
|
||||
antigravityEmailRegex = regexp.MustCompile(`[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}`)
|
||||
antigravityConsumerRegex = regexp.MustCompile(`(?i)\b(consumer|project(?:[ _-]?(?:id|number))?)(\s*[:=]?\s*['"]?)[0-9]{6,}`)
|
||||
)
|
||||
|
||||
// sanitizeAntigravityErrorText 清除上游错误文本中的 GCP 项目号/项目 ID、服务账号邮箱及敏感查询参数。
|
||||
func sanitizeAntigravityErrorText(msg string) string {
|
||||
if msg == "" {
|
||||
return msg
|
||||
}
|
||||
msg = sanitizeUpstreamErrorMessage(msg)
|
||||
msg = antigravityProjectRefRegex.ReplaceAllString(msg, "projects/***")
|
||||
msg = antigravityEmailRegex.ReplaceAllString(msg, "***")
|
||||
msg = antigravityConsumerRegex.ReplaceAllString(msg, "$1$2***")
|
||||
return msg
|
||||
}
|
||||
|
||||
// buildAntigravityClientErrorBody 为客户端构造 Gemini 风格的错误体:
|
||||
// 仅保留 code/status/message(message 已脱敏),丢弃 details 等可能含账号身份的字段。
|
||||
func buildAntigravityClientErrorBody(statusCode int, body []byte) []byte {
|
||||
code := statusCode
|
||||
status := ""
|
||||
message := ""
|
||||
|
||||
var parsed struct {
|
||||
Error struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Status string `json:"status"`
|
||||
} `json:"error"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &parsed); err == nil {
|
||||
if parsed.Error.Code != 0 {
|
||||
code = parsed.Error.Code
|
||||
}
|
||||
status = parsed.Error.Status
|
||||
message = parsed.Error.Message
|
||||
}
|
||||
if strings.TrimSpace(message) == "" {
|
||||
message = strings.TrimSpace(extractUpstreamErrorMessage(body))
|
||||
}
|
||||
if strings.TrimSpace(message) == "" {
|
||||
message = http.StatusText(statusCode)
|
||||
if message == "" {
|
||||
message = "Upstream request failed"
|
||||
}
|
||||
}
|
||||
if status == "" {
|
||||
status = antigravityGeminiStatusFromHTTP(statusCode)
|
||||
}
|
||||
|
||||
out, err := json.Marshal(map[string]any{
|
||||
"error": map[string]any{
|
||||
"code": code,
|
||||
"message": sanitizeAntigravityErrorText(message),
|
||||
"status": status,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return []byte(`{"error":{"code":500,"message":"Upstream request failed","status":"INTERNAL"}}`)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func antigravityGeminiStatusFromHTTP(statusCode int) string {
|
||||
switch statusCode {
|
||||
case http.StatusBadRequest:
|
||||
return "INVALID_ARGUMENT"
|
||||
case http.StatusUnauthorized:
|
||||
return "UNAUTHENTICATED"
|
||||
case http.StatusForbidden:
|
||||
return "PERMISSION_DENIED"
|
||||
case http.StatusNotFound:
|
||||
return "NOT_FOUND"
|
||||
case http.StatusTooManyRequests:
|
||||
return "RESOURCE_EXHAUSTED"
|
||||
case http.StatusServiceUnavailable:
|
||||
return "UNAVAILABLE"
|
||||
case http.StatusGatewayTimeout:
|
||||
return "DEADLINE_EXCEEDED"
|
||||
default:
|
||||
if statusCode >= 500 {
|
||||
return "INTERNAL"
|
||||
}
|
||||
return "UNKNOWN"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
//go:build unit
|
||||
|
||||
package service
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestBuildAntigravityClientErrorBody_ScrubsPoolIdentity(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
upstream := []byte(`{"error":{"code":403,"message":"Permission denied on resource project projects/123456789 for consumer: projects/123456789; caller pool-sa@my-gcp-proj.iam.gserviceaccount.com","status":"PERMISSION_DENIED","details":[{"@type":"type.googleapis.com/google.rpc.ErrorInfo","metadata":{"consumer":"projects/123456789","service":"cloudcode-pa.googleapis.com"}}]}}`)
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(rec)
|
||||
c.Data(http.StatusForbidden, "application/json", buildAntigravityClientErrorBody(http.StatusForbidden, upstream))
|
||||
|
||||
require.Equal(t, http.StatusForbidden, rec.Code)
|
||||
out := rec.Body.String()
|
||||
require.NotContains(t, out, "123456789")
|
||||
require.NotContains(t, out, "pool-sa@")
|
||||
require.NotContains(t, out, "gserviceaccount.com")
|
||||
require.NotContains(t, out, "details")
|
||||
|
||||
var parsed struct {
|
||||
Error struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Status string `json:"status"`
|
||||
} `json:"error"`
|
||||
}
|
||||
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &parsed))
|
||||
require.Equal(t, 403, parsed.Error.Code)
|
||||
require.Equal(t, "PERMISSION_DENIED", parsed.Error.Status)
|
||||
require.True(t, strings.Contains(parsed.Error.Message, "Permission denied"))
|
||||
}
|
||||
|
||||
func TestBuildAntigravityClientErrorBody_NonJSONBody(t *testing.T) {
|
||||
out := string(buildAntigravityClientErrorBody(http.StatusTooManyRequests, []byte("quota exceeded for consumer 987654321 sa@x.iam.gserviceaccount.com")))
|
||||
require.NotContains(t, out, "987654321")
|
||||
require.NotContains(t, out, "gserviceaccount")
|
||||
require.Contains(t, out, `"status":"RESOURCE_EXHAUSTED"`)
|
||||
require.Contains(t, out, `"code":429`)
|
||||
}
|
||||
Reference in New Issue
Block a user