feat: 完整游戏运维功能

This commit is contained in:
npc0-hue
2026-07-18 09:04:01 +08:00
parent f3b14b7945
commit 48b8ad8d6c
187 changed files with 16607 additions and 1140 deletions
+152 -10
View File
@@ -1,8 +1,14 @@
package service
import (
"crypto/hmac"
"crypto/sha256"
"crypto/subtle"
"encoding/hex"
"errors"
"fmt"
"strconv"
"strings"
"time"
"browser.local/platform/domain"
@@ -12,6 +18,9 @@ import (
const (
defaultHeartbeatIntervalSeconds = 15
defaultRunSessionTTL = 24 * time.Hour
maxRunRequestClockSkew = 5 * time.Minute
maxRunRequestNonces = 8192
)
func (svc *CoreService) RegisterRunHello(hello domain.RunControlHello) (domain.RunControlHelloResult, error) {
@@ -46,6 +55,8 @@ func (svc *CoreService) RegisterRunHello(hello domain.RunControlHello) (domain.R
ID: hello.RunEndpointID,
DisplayName: hello.DisplayName,
Version: hello.Version,
Platform: hello.Platform,
Architecture: hello.Architecture,
Status: domain.RunEndpointStatusOnline,
Capabilities: domain.CopyStringSlice(hello.CapabilityReport.Capabilities),
Capacity: hello.Capacity,
@@ -61,23 +72,53 @@ func (svc *CoreService) RegisterRunHello(hello domain.RunControlHello) (domain.R
if err := svc.upsertRunEndpoint(endpoint); err != nil {
return domain.RunControlHelloResult{}, err
}
sessionToken := svc.nextSessionToken(hello.RunEndpointID, stamp)
svc.runSessions[hello.RunEndpointID] = domain.RunControlSession{
previous, previousErr := svc.store.RunControlSessions().Get(hello.RunEndpointID)
generation := 1
if previousErr == nil {
generation = previous.Generation + 1
} else if !errors.Is(previousErr, repo.ErrNotFound) {
return domain.RunControlHelloResult{}, previousErr
}
sessionToken, err := svc.nextSessionToken()
if err != nil {
return domain.RunControlHelloResult{}, err
}
session := domain.RunControlSession{
RunEndpointID: hello.RunEndpointID,
SessionToken: sessionToken,
SessionTokenHash: tokenHash(sessionToken),
Status: domain.AuthSessionStatusActive,
Generation: generation,
CapabilityFingerprint: hello.CapabilityReport.Fingerprint,
HeartbeatIntervalSeconds: defaultHeartbeatIntervalSeconds,
CreatedAt: stamp,
UpdatedAt: stamp,
ExpiresAt: stamp.Add(defaultRunSessionTTL),
RequireSignedRequests: hasComponentAuthIdentity(hello),
}
if err := validator.ValidateRunControlSession(session); err != nil {
return domain.RunControlHelloResult{}, err
}
if previousErr == nil {
if err := svc.store.RunControlSessions().Update(session); err != nil {
return domain.RunControlHelloResult{}, err
}
} else if err := svc.store.RunControlSessions().Create(session); err != nil {
return domain.RunControlHelloResult{}, err
}
svc.runSessions[hello.RunEndpointID] = session
featureFlags := []string{"control.hello", "control.heartbeat", "signed-envelope.v1.optional"}
if session.RequireSignedRequests {
featureFlags[2] = "signed-envelope.v1.required"
}
return domain.CopyRunControlHelloResult(domain.RunControlHelloResult{
Accepted: true,
RunEndpointID: hello.RunEndpointID,
SessionToken: sessionToken,
ServerTime: stamp,
HeartbeatIntervalSeconds: defaultHeartbeatIntervalSeconds,
FeatureFlags: []string{"control.hello", "control.heartbeat"},
SessionExpiresAt: session.ExpiresAt,
FeatureFlags: featureFlags,
}), nil
}
@@ -96,9 +137,9 @@ func (svc *CoreService) AcceptRunHeartbeat(heartbeat domain.RunControlHeartbeat)
svc.controlMu.Lock()
defer svc.controlMu.Unlock()
session, exists := svc.runSessions[heartbeat.RunEndpointID]
if !exists || session.SessionToken != heartbeat.SessionToken {
return domain.RunControlHeartbeatResult{}, validationError("sessionToken is invalid")
session, err := svc.currentRunSession(heartbeat.RunEndpointID, heartbeat.SessionToken)
if err != nil {
return domain.RunControlHeartbeatResult{}, err
}
endpoint, err := svc.store.RunEndpoints().Get(heartbeat.RunEndpointID)
@@ -119,6 +160,9 @@ func (svc *CoreService) AcceptRunHeartbeat(heartbeat domain.RunControlHeartbeat)
refreshCapabilities := session.CapabilityFingerprint != heartbeat.CapabilityFingerprint
session.CapabilityFingerprint = heartbeat.CapabilityFingerprint
session.UpdatedAt = stamp
if err := svc.store.RunControlSessions().Update(session); err != nil {
return domain.RunControlHeartbeatResult{}, err
}
svc.runSessions[heartbeat.RunEndpointID] = session
return domain.CopyRunControlHeartbeatResult(domain.RunControlHeartbeatResult{
@@ -140,7 +184,105 @@ func (svc *CoreService) upsertRunEndpoint(endpoint domain.RunEndpoint) error {
return svc.store.RunEndpoints().Update(endpoint)
}
func (svc *CoreService) nextSessionToken(runEndpointID string, stamp time.Time) string {
svc.runSessionSeq++
return fmt.Sprintf("session:%s:%d:%d", runEndpointID, stamp.UnixNano(), svc.runSessionSeq)
func (svc *CoreService) nextSessionToken() (string, error) {
return randomToken()
}
func (svc *CoreService) currentRunSession(runEndpointID string, sessionToken string) (domain.RunControlSession, error) {
session, exists := svc.runSessions[runEndpointID]
if !exists {
stored, err := svc.store.RunControlSessions().Get(runEndpointID)
if err != nil {
return domain.RunControlSession{}, runAuthenticationError(true)
}
session = stored
}
presentedHash := tokenHash(strings.TrimSpace(sessionToken))
if strings.TrimSpace(sessionToken) == "" || session.Status != domain.AuthSessionStatusActive || !session.RevokedAt.IsZero() || !svc.now().Before(session.ExpiresAt) || subtle.ConstantTimeCompare([]byte(session.SessionTokenHash), []byte(presentedHash)) != 1 {
if session.Status == domain.AuthSessionStatusActive && !svc.now().Before(session.ExpiresAt) {
session.Status = domain.AuthSessionStatusRevoked
session.RevokedAt = svc.now()
session.UpdatedAt = session.RevokedAt
_ = svc.store.RunControlSessions().Update(session)
}
return domain.RunControlSession{}, runAuthenticationError(session.RequireSignedRequests)
}
return domain.CopyRunControlSession(session), nil
}
func runAuthenticationError(requireSigned bool) error {
if !requireSigned {
return validationError("sessionToken is invalid")
}
return fmt.Errorf("sessionToken is invalid: %w", ErrUnauthorized)
}
func (svc *CoreService) AuthorizeRunRequestSignature(request domain.RunRequestSignature) error {
svc.controlMu.Lock()
defer svc.controlMu.Unlock()
session, err := svc.currentRunSession(request.RunEndpointID, request.SessionToken)
if err != nil {
return err
}
if strings.TrimSpace(request.Signature) == "" && !session.RequireSignedRequests {
return nil
}
if strings.TrimSpace(request.Timestamp) == "" || strings.TrimSpace(request.Nonce) == "" || strings.TrimSpace(request.Signature) == "" {
return runAuthenticationError(true)
}
unixSeconds, err := strconv.ParseInt(request.Timestamp, 10, 64)
if err != nil {
return runAuthenticationError(true)
}
stamp := time.Unix(unixSeconds, 0).UTC()
delta := svc.now().Sub(stamp)
if delta < -maxRunRequestClockSkew || delta > maxRunRequestClockSkew {
return runAuthenticationError(true)
}
session.UsedNonces = activeRunNonces(session.UsedNonces, svc.now().Add(-maxRunRequestClockSkew))
if len(request.Nonce) > 128 || runNonceSeen(session.UsedNonces, request.Nonce) || len(session.UsedNonces) >= maxRunRequestNonces {
return runAuthenticationError(true)
}
canonical := strings.Join([]string{request.Method, request.Path, request.Timestamp, request.Nonce, request.BodyHash}, "\n")
mac := hmac.New(sha256.New, []byte(request.SessionToken))
_, _ = mac.Write([]byte(canonical))
expected := hex.EncodeToString(mac.Sum(nil))
provided, err := hex.DecodeString(request.Signature)
if err != nil || subtle.ConstantTimeCompare([]byte(expected), []byte(hex.EncodeToString(provided))) != 1 {
return runAuthenticationError(true)
}
session.UsedNonces = append(session.UsedNonces, request.Timestamp+":"+request.Nonce)
session.UpdatedAt = svc.now()
if err := svc.store.RunControlSessions().Update(session); err != nil {
return err
}
svc.runSessions[request.RunEndpointID] = session
return nil
}
func activeRunNonces(entries []string, cutoff time.Time) []string {
active := make([]string, 0, len(entries))
for _, entry := range entries {
timestamp, _, ok := strings.Cut(entry, ":")
if !ok {
active = append(active, entry)
continue
}
unixSeconds, err := strconv.ParseInt(timestamp, 10, 64)
if err == nil && !time.Unix(unixSeconds, 0).Before(cutoff) {
active = append(active, entry)
}
}
return active
}
func runNonceSeen(entries []string, nonce string) bool {
for _, entry := range entries {
_, storedNonce, ok := strings.Cut(entry, ":")
if (ok && storedNonce == nonce) || (!ok && entry == nonce) {
return true
}
}
return false
}