将 Worker 发现、路由画像、容量与执行传输抽象为平台无关接口,新增 Kubernetes 和静态容量适配器,并以 shadow 模式接入生产配置。 实现网络与容量评分、路由防抖、池队列、同步 Worker 租约、一次性执行令牌,以及提交状态不明时禁止重复分配的安全语义。 新增 0105 兼容迁移、管理接口、指标、OpenAPI 和回归测试。已执行全量 Go 测试、go vet、OpenAPI、迁移安全、Compose 与 Kustomize 验证。
138 lines
4.2 KiB
Go
138 lines
4.2 KiB
Go
package executionpool
|
|
|
|
import (
|
|
"crypto/hmac"
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
func QueueName(poolID string) string {
|
|
poolID = strings.TrimSpace(poolID)
|
|
if poolID == "" {
|
|
return "gateway_tasks"
|
|
}
|
|
digest := sha256.Sum256([]byte(poolID))
|
|
return "gateway_pool_" + fmt.Sprintf("%x", digest[:8])
|
|
}
|
|
|
|
func RouteProfileKey(provider, protocol, endpointHost, proxyMode, configRevision string) string {
|
|
value := strings.Join([]string{
|
|
strings.ToLower(strings.TrimSpace(provider)),
|
|
strings.ToLower(strings.TrimSpace(protocol)),
|
|
strings.ToLower(strings.TrimSpace(endpointHost)),
|
|
strings.ToLower(strings.TrimSpace(proxyMode)),
|
|
strings.TrimSpace(configRevision),
|
|
}, "\x00")
|
|
digest := sha256.Sum256([]byte(value))
|
|
return fmt.Sprintf("route_%x", digest[:16])
|
|
}
|
|
|
|
func EndpointHost(rawURL string) string {
|
|
parsed, err := url.Parse(strings.TrimSpace(rawURL))
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return strings.ToLower(parsed.Hostname())
|
|
}
|
|
|
|
func ValidateAdvertisedEndpoint(rawURL string, allowedSuffixes []string, allowPrivate bool) error {
|
|
parsed, err := url.Parse(strings.TrimSpace(rawURL))
|
|
if err != nil || parsed.Host == "" || parsed.User != nil {
|
|
return errors.New("worker endpoint must be an absolute URL without user info")
|
|
}
|
|
if parsed.Scheme != "https" && parsed.Scheme != "http" {
|
|
return errors.New("worker endpoint scheme must be http or https")
|
|
}
|
|
host := strings.ToLower(parsed.Hostname())
|
|
if host == "" {
|
|
return errors.New("worker endpoint host is required")
|
|
}
|
|
if ip := net.ParseIP(host); ip != nil {
|
|
if allowPrivate && (ip.IsPrivate() || ip.IsLoopback()) {
|
|
return nil
|
|
}
|
|
return errors.New("worker endpoint IP is outside the configured trust boundary")
|
|
}
|
|
for _, suffix := range allowedSuffixes {
|
|
suffix = strings.ToLower(strings.TrimSpace(suffix))
|
|
if suffix != "" && (host == strings.TrimPrefix(suffix, ".") || strings.HasSuffix(host, "."+strings.TrimPrefix(suffix, "."))) {
|
|
return nil
|
|
}
|
|
}
|
|
return errors.New("worker endpoint host is outside the configured trust boundary")
|
|
}
|
|
|
|
type ExecutionClaims struct {
|
|
Audience string `json:"aud"`
|
|
TaskID string `json:"task_id"`
|
|
PoolID string `json:"pool_id"`
|
|
WorkerID string `json:"worker_id"`
|
|
Nonce string `json:"nonce"`
|
|
ExpiresAt int64 `json:"exp"`
|
|
}
|
|
|
|
type TokenSigner struct {
|
|
Secret []byte
|
|
Now func() time.Time
|
|
}
|
|
|
|
func (s TokenSigner) Sign(claims ExecutionClaims) (string, error) {
|
|
if len(s.Secret) < 32 {
|
|
return "", errors.New("execution token secret must be at least 32 bytes")
|
|
}
|
|
if claims.Audience == "" || claims.TaskID == "" || claims.PoolID == "" || claims.WorkerID == "" || claims.Nonce == "" || claims.ExpiresAt <= 0 {
|
|
return "", errors.New("execution token claims are incomplete")
|
|
}
|
|
payload, err := json.Marshal(claims)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
encoded := base64.RawURLEncoding.EncodeToString(payload)
|
|
mac := hmac.New(sha256.New, s.Secret)
|
|
_, _ = mac.Write([]byte(encoded))
|
|
signature := base64.RawURLEncoding.EncodeToString(mac.Sum(nil))
|
|
return encoded + "." + signature, nil
|
|
}
|
|
|
|
func (s TokenSigner) Verify(token, audience string) (ExecutionClaims, error) {
|
|
var claims ExecutionClaims
|
|
if len(s.Secret) < 32 {
|
|
return claims, errors.New("execution token secret must be at least 32 bytes")
|
|
}
|
|
parts := strings.Split(token, ".")
|
|
if len(parts) != 2 {
|
|
return claims, errors.New("invalid execution token")
|
|
}
|
|
signature, err := base64.RawURLEncoding.DecodeString(parts[1])
|
|
if err != nil {
|
|
return claims, errors.New("invalid execution token signature")
|
|
}
|
|
mac := hmac.New(sha256.New, s.Secret)
|
|
_, _ = mac.Write([]byte(parts[0]))
|
|
if !hmac.Equal(signature, mac.Sum(nil)) {
|
|
return claims, errors.New("invalid execution token signature")
|
|
}
|
|
payload, err := base64.RawURLEncoding.DecodeString(parts[0])
|
|
if err != nil || json.Unmarshal(payload, &claims) != nil {
|
|
return ExecutionClaims{}, errors.New("invalid execution token payload")
|
|
}
|
|
now := time.Now()
|
|
if s.Now != nil {
|
|
now = s.Now()
|
|
}
|
|
if claims.ExpiresAt <= now.Unix() {
|
|
return ExecutionClaims{}, errors.New("execution token expired")
|
|
}
|
|
if claims.Audience != audience {
|
|
return ExecutionClaims{}, errors.New("execution token audience mismatch")
|
|
}
|
|
return claims, nil
|
|
}
|