147 lines
4.9 KiB
Go
147 lines
4.9 KiB
Go
package workerruntime
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/json"
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
const (
|
|
maximumLabels = 32
|
|
maximumLabelKeyBytes = 64
|
|
maximumLabelValueBytes = 256
|
|
maximumLabelTotalBytes = 4 << 10
|
|
)
|
|
|
|
var identifierPattern = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$`)
|
|
|
|
// ValidIdentifier accepts stable Worker, Session and Proxy identifiers.
|
|
func ValidIdentifier(value string) bool {
|
|
return identifierPattern.MatchString(value)
|
|
}
|
|
|
|
// NormalizeLabels validates and deep-copies the bounded Worker label set.
|
|
func NormalizeLabels(labels map[string]string) (map[string]string, error) {
|
|
if len(labels) > maximumLabels {
|
|
return nil, ErrInvalidSession
|
|
}
|
|
normalized := make(map[string]string, len(labels))
|
|
total := 0
|
|
for key, value := range labels {
|
|
if !ValidIdentifier(key) || len(key) > maximumLabelKeyBytes || value == "" ||
|
|
strings.TrimSpace(value) != value || strings.IndexByte(value, 0) >= 0 ||
|
|
len(value) > maximumLabelValueBytes {
|
|
return nil, ErrInvalidSession
|
|
}
|
|
total += len(key) + len(value)
|
|
if total > maximumLabelTotalBytes {
|
|
return nil, ErrInvalidSession
|
|
}
|
|
normalized[key] = value
|
|
}
|
|
return normalized, nil
|
|
}
|
|
|
|
// NormalizeSession prepares a new, not-yet-acknowledged session for storage.
|
|
func NormalizeSession(session Session) (Session, error) {
|
|
if !ValidIdentifier(session.WorkerID) || !ValidIdentifier(session.InstanceID) ||
|
|
!ValidIdentifier(session.SessionID) || !ValidIdentifier(session.Zone) ||
|
|
session.ProtocolVersion == 0 || session.AckedSnapshotVersion != 0 ||
|
|
session.AckedOwnershipEpoch != 0 || !checksumIsZero(session.AckedChecksum) ||
|
|
session.RuntimeEnabled {
|
|
return Session{}, ErrInvalidSession
|
|
}
|
|
labels, err := NormalizeLabels(session.Labels)
|
|
if err != nil {
|
|
return Session{}, err
|
|
}
|
|
session.Labels = labels
|
|
return session, nil
|
|
}
|
|
|
|
func NormalizeSnapshotReference(reference SnapshotReference) (SnapshotReference, error) {
|
|
if !ValidIdentifier(reference.WorkerID) || reference.Version == 0 || reference.OwnershipEpoch == 0 ||
|
|
checksumIsZero(reference.Checksum) {
|
|
return SnapshotReference{}, ErrInvalidSnapshotReference
|
|
}
|
|
return reference, nil
|
|
}
|
|
|
|
func NormalizeAcknowledgement(acknowledgement SnapshotAcknowledgement) (SnapshotAcknowledgement, error) {
|
|
if !ValidIdentifier(acknowledgement.WorkerID) || !ValidIdentifier(acknowledgement.SessionID) ||
|
|
(acknowledgement.ErrorCode != "" && !ValidIdentifier(acknowledgement.ErrorCode)) {
|
|
return SnapshotAcknowledgement{}, ErrInvalidAcknowledgement
|
|
}
|
|
reference, err := NormalizeSnapshotReference(acknowledgement.Reference)
|
|
if err != nil || reference.WorkerID != acknowledgement.WorkerID {
|
|
return SnapshotAcknowledgement{}, ErrInvalidAcknowledgement
|
|
}
|
|
acknowledgement.Reference = reference
|
|
return acknowledgement, nil
|
|
}
|
|
|
|
func NormalizeOutcomeReference(reference OutcomeReference) (OutcomeReference, error) {
|
|
if !ValidIdentifier(reference.WorkerID) || !ValidIdentifier(reference.SessionID) || reference.Sequence == 0 ||
|
|
checksumIsZero(reference.Digest) {
|
|
return OutcomeReference{}, ErrInvalidOutcome
|
|
}
|
|
return reference, nil
|
|
}
|
|
|
|
// NormalizeReport returns the canonical sparse replacement and its digest.
|
|
func NormalizeReport(report Report) (Report, [sha256.Size]byte, error) {
|
|
if !ValidIdentifier(report.WorkerID) || !ValidIdentifier(report.SessionID) || report.Sequence == 0 ||
|
|
report.SnapshotVersion == 0 || report.OwnershipEpoch == 0 || report.ObservedAt.IsZero() {
|
|
return Report{}, [sha256.Size]byte{}, ErrInvalidReport
|
|
}
|
|
normalized := report
|
|
normalized.ObservedAt = report.ObservedAt.UTC()
|
|
normalized.Counters = append([]Counter(nil), report.Counters...)
|
|
sort.Slice(normalized.Counters, func(left, right int) bool {
|
|
return normalized.Counters[left].ProxyID < normalized.Counters[right].ProxyID
|
|
})
|
|
seen := make(map[string]struct{}, len(normalized.Counters))
|
|
for _, counter := range normalized.Counters {
|
|
if !ValidIdentifier(counter.ProxyID) || counter.Active < 0 || counter.Reserved < 0 {
|
|
return Report{}, [sha256.Size]byte{}, ErrInvalidReport
|
|
}
|
|
if _, exists := seen[counter.ProxyID]; exists {
|
|
return Report{}, [sha256.Size]byte{}, ErrInvalidReport
|
|
}
|
|
seen[counter.ProxyID] = struct{}{}
|
|
}
|
|
payload, err := json.Marshal(normalized)
|
|
if err != nil {
|
|
return Report{}, [sha256.Size]byte{}, ErrInvalidReport
|
|
}
|
|
return normalized, sha256.Sum256(payload), nil
|
|
}
|
|
|
|
func checksumIsZero(checksum [sha256.Size]byte) bool {
|
|
return checksum == [sha256.Size]byte{}
|
|
}
|
|
|
|
func compareSnapshotTuple(left, right SnapshotReference) int {
|
|
switch {
|
|
case left.OwnershipEpoch < right.OwnershipEpoch:
|
|
return -1
|
|
case left.OwnershipEpoch > right.OwnershipEpoch:
|
|
return 1
|
|
case left.Version < right.Version:
|
|
return -1
|
|
case left.Version > right.Version:
|
|
return 1
|
|
default:
|
|
return 0
|
|
}
|
|
}
|
|
|
|
func sessionReference(session Session) SnapshotReference {
|
|
return SnapshotReference{
|
|
WorkerID: session.WorkerID, Version: session.AckedSnapshotVersion,
|
|
OwnershipEpoch: session.AckedOwnershipEpoch, Checksum: session.AckedChecksum,
|
|
}
|
|
}
|