功能修改
This commit is contained in:
@@ -0,0 +1,184 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"crypto/subtle"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"browser.local/platform/domain"
|
||||
"browser.local/platform/repo"
|
||||
"browser.local/platform/validator"
|
||||
)
|
||||
|
||||
const gameClientBridgeCapability = "game-client.bridge"
|
||||
|
||||
func (svc *CoreService) ClaimGameClientBridgeCommands(request domain.GameClientBridgeClaimRequest) ([]domain.GameClientBridgeCommand, error) {
|
||||
if err := validator.ValidateGameClientBridgeClaimRequest(request); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
component, err := svc.authorizeGameClientBridgeSession(request.SessionToken)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return svc.claimGameClientBridgeCommands(component, request.Limit)
|
||||
}
|
||||
|
||||
func (svc *CoreService) AckGameClientBridgeCommand(request domain.GameClientBridgeAckRequest) (domain.GameClientBridgeCommand, error) {
|
||||
if err := validator.ValidateGameClientBridgeAckRequest(request); err != nil {
|
||||
return domain.GameClientBridgeCommand{}, err
|
||||
}
|
||||
component, err := svc.authorizeGameClientBridgeSession(request.SessionToken)
|
||||
if err != nil {
|
||||
return domain.GameClientBridgeCommand{}, err
|
||||
}
|
||||
return svc.ackGameClientBridgeCommand(component, request)
|
||||
}
|
||||
|
||||
func (svc *CoreService) CompleteGameClientBridgeCommand(request domain.GameClientBridgeResultRequest) (domain.GameClientBridgeCommand, error) {
|
||||
if err := validator.ValidateGameClientBridgeResultRequest(request); err != nil {
|
||||
return domain.GameClientBridgeCommand{}, err
|
||||
}
|
||||
component, err := svc.authorizeGameClientBridgeSession(request.SessionToken)
|
||||
if err != nil {
|
||||
return domain.GameClientBridgeCommand{}, err
|
||||
}
|
||||
return svc.completeGameClientBridgeCommand(component, request)
|
||||
}
|
||||
|
||||
func (svc *CoreService) UploadGameClientBridgeSnapshot(request domain.GameClientBridgeSnapshotIngestRequest) (domain.GameClientBridgeSnapshot, error) {
|
||||
request.Payload = domain.CopyGameClientBridgePayload(request.Payload)
|
||||
if err := validator.ValidateGameClientBridgeSnapshotIngestRequest(request); err != nil {
|
||||
return domain.GameClientBridgeSnapshot{}, err
|
||||
}
|
||||
component, err := svc.authorizeGameClientBridgeSession(request.SessionToken)
|
||||
if err != nil {
|
||||
return domain.GameClientBridgeSnapshot{}, err
|
||||
}
|
||||
stamp := svc.now()
|
||||
if request.ObservedAt.After(stamp.Add(5 * time.Minute)) {
|
||||
return domain.GameClientBridgeSnapshot{}, validationError("snapshot observedAt is too far in the future")
|
||||
}
|
||||
plugin, err := svc.store.GamePlugins().Get(component.Installation.PluginID)
|
||||
if err != nil {
|
||||
return domain.GameClientBridgeSnapshot{}, err
|
||||
}
|
||||
declaration, declared := gameClientBridgeSnapshotDeclaration(plugin, request.Type, request.SchemaVersion)
|
||||
if !declared {
|
||||
return domain.GameClientBridgeSnapshot{}, validationError("bridge snapshot type and schema version are not declared")
|
||||
}
|
||||
if request.Retention != declaration.Retention {
|
||||
return domain.GameClientBridgeSnapshot{}, validationError("bridge snapshot retention does not match declaration")
|
||||
}
|
||||
svc.bridgeMu.Lock()
|
||||
defer svc.bridgeMu.Unlock()
|
||||
|
||||
streamID := gameClientBridgeStreamID(component.Session.ServerInstanceID, component.Installation.PluginID, component.Session.ProfileKey, request.Type, request.StreamKey)
|
||||
latestSequence := uint64(0)
|
||||
stream, streamErr := svc.store.GameClientBridgeSnapshotStreams().Get(streamID)
|
||||
if streamErr == nil {
|
||||
latestSequence = stream.LatestSequence
|
||||
} else if streamErr != repo.ErrNotFound {
|
||||
return domain.GameClientBridgeSnapshot{}, streamErr
|
||||
}
|
||||
existing, err := svc.store.GameClientBridgeSnapshots().List(domain.GameClientBridgeSnapshotFilter{ServerInstanceID: component.Session.ServerInstanceID, PluginID: component.Installation.PluginID, ProfileKey: component.Session.ProfileKey, Type: request.Type, StreamKey: request.StreamKey})
|
||||
if err != nil {
|
||||
return domain.GameClientBridgeSnapshot{}, err
|
||||
}
|
||||
for _, snapshot := range existing {
|
||||
if snapshot.Sequence > latestSequence {
|
||||
latestSequence = snapshot.Sequence
|
||||
}
|
||||
}
|
||||
if request.Sequence <= latestSequence {
|
||||
return domain.GameClientBridgeSnapshot{}, validationError("snapshot sequence is stale")
|
||||
}
|
||||
svc.bridgeSeq++
|
||||
snapshot := domain.GameClientBridgeSnapshot{
|
||||
ID: fmt.Sprintf("bridge-snapshot-%d-%d", stamp.UnixNano(), svc.bridgeSeq),
|
||||
ServerInstanceID: component.Session.ServerInstanceID,
|
||||
PluginID: component.Installation.PluginID,
|
||||
ProfileKey: component.Session.ProfileKey,
|
||||
Type: request.Type,
|
||||
SchemaVersion: request.SchemaVersion,
|
||||
StreamKey: request.StreamKey,
|
||||
Sequence: request.Sequence,
|
||||
SourceSessionID: component.Session.ID,
|
||||
ObservedAt: request.ObservedAt,
|
||||
Payload: domain.CopyGameClientBridgePayload(request.Payload),
|
||||
Retention: request.Retention,
|
||||
CreatedAt: stamp,
|
||||
ExpiresAt: stamp.Add(time.Duration(request.Retention.KeepForSeconds) * time.Second),
|
||||
}
|
||||
auditID, err := svc.recordAuditEventWithID("component:"+component.Session.ProfileKey, "game-client-bridge.snapshot.ingest", "game-client-bridge-snapshot", snapshot.ID, domain.AuditResultSuccess, "companion uploaded typed bridge snapshot")
|
||||
if err != nil {
|
||||
return domain.GameClientBridgeSnapshot{}, err
|
||||
}
|
||||
snapshot.AuditReferences = []string{auditID}
|
||||
if err := svc.store.GameClientBridgeSnapshots().Create(snapshot); err != nil {
|
||||
return domain.GameClientBridgeSnapshot{}, err
|
||||
}
|
||||
stream = domain.GameClientBridgeSnapshotStream{ID: streamID, ServerInstanceID: snapshot.ServerInstanceID, PluginID: snapshot.PluginID, ProfileKey: snapshot.ProfileKey, Type: snapshot.Type, StreamKey: snapshot.StreamKey, LatestSequence: snapshot.Sequence, UpdatedAt: stamp}
|
||||
if streamErr == repo.ErrNotFound {
|
||||
if err := svc.store.GameClientBridgeSnapshotStreams().Create(stream); err != nil {
|
||||
return domain.GameClientBridgeSnapshot{}, err
|
||||
}
|
||||
} else {
|
||||
if err := svc.store.GameClientBridgeSnapshotStreams().Update(stream); err != nil {
|
||||
return domain.GameClientBridgeSnapshot{}, err
|
||||
}
|
||||
}
|
||||
return domain.CopyGameClientBridgeSnapshot(snapshot), nil
|
||||
}
|
||||
|
||||
func gameClientBridgeSnapshotDeclaration(plugin domain.GamePlugin, snapshotType, schemaVersion string) (domain.GameClientBridgeSnapshotDeclaration, bool) {
|
||||
for _, declaration := range plugin.GameClientBridge.Snapshots {
|
||||
if declaration.Type == snapshotType && declaration.SchemaVersion == schemaVersion {
|
||||
return declaration, true
|
||||
}
|
||||
}
|
||||
return domain.GameClientBridgeSnapshotDeclaration{}, false
|
||||
}
|
||||
|
||||
func (svc *CoreService) authorizeGameClientBridgeSession(sessionToken string) (gameClientBridgeComponentSession, error) {
|
||||
presentedHash := tokenHash(sessionToken)
|
||||
sessions, err := svc.store.ClientManagerSessions().List(domain.ClientManagerSessionFilter{Status: domain.ClientManagerSessionActive})
|
||||
if err != nil {
|
||||
return gameClientBridgeComponentSession{}, err
|
||||
}
|
||||
var session domain.ClientManagerSession
|
||||
for _, candidate := range sessions {
|
||||
if subtle.ConstantTimeCompare([]byte(candidate.TokenHash), []byte(presentedHash)) == 1 {
|
||||
session = candidate
|
||||
break
|
||||
}
|
||||
}
|
||||
stamp := svc.now()
|
||||
if session.ID == "" || !stamp.Before(session.ExpiresAt) {
|
||||
return gameClientBridgeComponentSession{}, ErrUnauthorized
|
||||
}
|
||||
if !containsString(session.Capabilities, gameClientBridgeCapability) {
|
||||
return gameClientBridgeComponentSession{}, ErrForbidden
|
||||
}
|
||||
installation, err := svc.store.ClientManagerInstallations().Get(session.InstallationID)
|
||||
if err != nil {
|
||||
return gameClientBridgeComponentSession{}, ErrUnauthorized
|
||||
}
|
||||
if installation.Status != domain.ClientManagerLifecycleOnline && installation.Status != domain.ClientManagerLifecycleDegraded {
|
||||
return gameClientBridgeComponentSession{}, ErrUnauthorized
|
||||
}
|
||||
if installation.ServerInstanceID != session.ServerInstanceID || installation.ProfileKey != session.ProfileKey || installation.RunEndpointID != session.RunEndpointID || installation.ActiveArtifactID != session.ArtifactID || installation.KeyGeneration != session.KeyGeneration || installation.DeploymentGeneration != session.DeploymentGeneration || installation.RequiresRedeploy {
|
||||
return gameClientBridgeComponentSession{}, ErrUnauthorized
|
||||
}
|
||||
key, err := svc.activeComponentKey(session.ServerInstanceID, domain.DistributionComponentClientManager, session.ProfileKey)
|
||||
if err != nil || key.Generation != session.KeyGeneration {
|
||||
return gameClientBridgeComponentSession{}, ErrUnauthorized
|
||||
}
|
||||
return gameClientBridgeComponentSession{Session: domain.CopyClientManagerSession(session), Installation: domain.CopyClientManagerInstallation(installation)}, nil
|
||||
}
|
||||
|
||||
func gameClientBridgeStreamID(serverInstanceID, pluginID, profileKey, snapshotType, streamKey string) string {
|
||||
digest := sha256.Sum256([]byte(serverInstanceID + "\x00" + pluginID + "\x00" + profileKey + "\x00" + snapshotType + "\x00" + streamKey))
|
||||
return "bridge-stream-" + hex.EncodeToString(digest[:16])
|
||||
}
|
||||
Reference in New Issue
Block a user