refactor(core): 任务与模型协议治理 - 拆分服务职责并禁止已发起任务取消
This commit is contained in:
@@ -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")
|
||||
|
||||
@@ -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" {
|
||||
|
||||
Reference in New Issue
Block a user