refactor(core): 任务与模型协议治理 - 拆分服务职责并禁止已发起任务取消

This commit is contained in:
ddCat
2026-08-22 09:52:09 +08:00
parent 7418ddfa9c
commit 633560a5d8
91 changed files with 6380 additions and 4247 deletions

View File

@@ -67,7 +67,11 @@ func main() {
r.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
return fmt.Sprintf("%s - [%s] \"%s %s\" %d %s %s\n", param.ClientIP, param.TimeStamp.Format(time.RFC3339), param.Method, redactCanvasSharePath(param.Path), param.StatusCode, param.Latency, param.ErrorMessage)
}), gin.Recovery())
r.Use(cors())
corsMiddleware, err := cors()
if err != nil {
log.Fatal(err)
}
r.Use(corsMiddleware)
handler.ConfigureRuntime(svc)
api := r.Group("/api")
api.GET("/health", func(c *gin.Context) {
@@ -123,10 +127,23 @@ func env(key string, fallback string) string {
return value
}
func cors() gin.HandlerFunc {
const corsAllowedHeaders = "Accept, Content-Type, Authorization, X-Requested-With, X-Canvas-Scene, X-Idempotency-Key, X-Canvas-Upstream-URL, X-Canvas-Upstream-Format, X-Canvas-Allow-Local-Channel, X-Canvas-Upstream-Base-URL"
const corsAllowedMethods = "GET, POST, PUT, PATCH, DELETE, OPTIONS"
type corsPolicy struct {
origins map[string]struct{}
allowAny bool
}
func cors() (gin.HandlerFunc, error) {
policy, err := parseCORSPolicy(os.Getenv("CANVAS_CORS_ORIGINS"))
if err != nil {
return nil, err
}
return func(c *gin.Context) {
origin := strings.TrimSpace(c.GetHeader("Origin"))
if origin != "" && !allowedOrigin(c, origin) {
if origin != "" && !allowedOriginWithPolicy(c, origin, policy) {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"code": http.StatusForbidden, "data": nil, "msg": "不允许的跨域来源"})
return
}
@@ -135,20 +152,67 @@ func cors() gin.HandlerFunc {
c.Header("Access-Control-Allow-Credentials", "true")
c.Header("Vary", "Origin, Access-Control-Request-Method, Access-Control-Request-Headers")
}
c.Header("Access-Control-Allow-Headers", "Accept, Content-Type, Authorization, X-Requested-With, X-Canvas-Scene, X-Idempotency-Key, X-Canvas-Upstream-URL, X-Canvas-Upstream-Format, X-Canvas-Allow-Local-Channel, X-Canvas-Upstream-Base-URL")
c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS")
c.Header("Access-Control-Allow-Headers", corsAllowedHeaders)
c.Header("Access-Control-Allow-Methods", corsAllowedMethods)
c.Header("Access-Control-Max-Age", "86400")
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(204)
return
}
c.Next()
}
}, nil
}
func allowedOrigin(c *gin.Context, origin string) bool {
parsed, err := url.Parse(origin)
if err != nil || parsed.Host == "" || (parsed.Scheme != "http" && parsed.Scheme != "https") {
policy, err := parseCORSPolicy(os.Getenv("CANVAS_CORS_ORIGINS"))
if err != nil {
return false
}
return allowedOriginWithPolicy(c, origin, policy)
}
func parseCORSPolicy(raw string) (corsPolicy, error) {
policy := corsPolicy{origins: make(map[string]struct{})}
for _, value := range strings.Split(raw, ",") {
value = strings.TrimSpace(value)
if value == "" {
continue
}
if value == "*" {
policy.allowAny = true
continue
}
normalized, err := normalizeCORSOrigin(value)
if err != nil {
return corsPolicy{}, fmt.Errorf("CANVAS_CORS_ORIGINS contains invalid origin %q: %w", value, err)
}
policy.origins[normalized] = struct{}{}
}
return policy, nil
}
func normalizeCORSOrigin(raw string) (string, error) {
value := strings.TrimSpace(raw)
if value == "" {
return "", fmt.Errorf("origin is empty")
}
parsed, err := url.Parse(value)
if err != nil || parsed.Host == "" || parsed.User != nil || (parsed.Scheme != "http" && parsed.Scheme != "https") {
return "", fmt.Errorf("origin must be an http or https origin")
}
if parsed.Path != "" && parsed.Path != "/" || parsed.RawQuery != "" || parsed.Fragment != "" {
return "", fmt.Errorf("origin must not contain a path, query, or fragment")
}
return strings.ToLower(parsed.Scheme) + "://" + strings.ToLower(parsed.Host), nil
}
func allowedOriginWithPolicy(c *gin.Context, origin string, policy corsPolicy) bool {
normalizedOrigin, err := normalizeCORSOrigin(origin)
if err != nil {
return false
}
parsed, err := url.Parse(normalizedOrigin)
if err != nil {
return false
}
requestHost := c.Request.Host
@@ -158,13 +222,14 @@ func allowedOrigin(c *gin.Context, origin string) bool {
if strings.EqualFold(parsed.Host, strings.TrimSpace(requestHost)) {
return true
}
for _, allowed := range strings.Split(os.Getenv("CANVAS_CORS_ORIGINS"), ",") {
if strings.TrimSpace(allowed) == "*" {
return true
}
if strings.EqualFold(strings.TrimRight(strings.TrimSpace(allowed), "/"), strings.TrimRight(origin, "/")) {
return true
}
if policy.allowAny {
return true
}
if _, ok := policy.origins[normalizedOrigin]; ok {
return true
}
if len(policy.origins) > 0 {
return false
}
host := strings.ToLower(parsed.Hostname())
return (host == "localhost" || host == "127.0.0.1" || host == "::1") && (parsed.Scheme == "http" || parsed.Scheme == "https")

View File

@@ -29,6 +29,37 @@ func TestAllowedOriginUsesForwardedHost(t *testing.T) {
}
}
func TestParseCORSPolicyNormalizesConfiguredOrigins(t *testing.T) {
policy, err := parseCORSPolicy(" https://Canvas.example.com/ , http://localhost:3000 ")
if err != nil {
t.Fatal(err)
}
if _, ok := policy.origins["https://canvas.example.com"]; !ok {
t.Fatal("configured origin was not normalized")
}
if _, ok := policy.origins["http://localhost:3000"]; !ok {
t.Fatal("configured port was not preserved")
}
}
func TestParseCORSPolicyRejectsNonOriginValue(t *testing.T) {
if _, err := parseCORSPolicy("https://example.com/app"); err == nil {
t.Fatal("path-bearing CORS value should be rejected")
}
if _, err := parseCORSPolicy("ftp://example.com"); err == nil {
t.Fatal("non-HTTP CORS value should be rejected")
}
}
func TestAllowedOriginConfiguredListDoesNotFallbackToArbitraryLocalhost(t *testing.T) {
t.Setenv("CANVAS_CORS_ORIGINS", "https://app.example.com")
context, _ := gin.CreateTestContext(httptest.NewRecorder())
context.Request = httptest.NewRequest("GET", "http://backend/api/health", nil)
if allowedOrigin(context, "http://localhost:3000") {
t.Fatal("configured CORS list should disable the implicit localhost fallback")
}
}
func TestRedactCanvasSharePath(t *testing.T) {
got := redactCanvasSharePath("/api/public/canvas-shares/private-token/resources/resource-1/file")
if got != "/api/public/canvas-shares/:token/resources/resource-1/file" {