feat: validate queue compatibility metadata

This commit is contained in:
Josh Creek
2026-08-31 21:57:40 +01:00
parent 0551fb0b1f
commit b2ee9ec92d
6 changed files with 126 additions and 18 deletions
+27 -5
View File
@@ -19,12 +19,14 @@ import (
const maxBodyBytes = 8 << 10
type CandidateProvider func(playerID, ticketID string) (domain.Candidate, error)
type CandidateProviderV2 func(playerID, ticketID string, spec domain.QueueSpec) (domain.Candidate, error)
type ProbeProvider func(playerID, region string, opaqueLocation, nonce []byte, receivedAt time.Time) (domain.ProbeEvidence, []byte, error)
type Service struct {
Sessions *domain.SessionStore
Queue *domain.Queue
Candidate CandidateProvider
CandidateV2 CandidateProviderV2
Probe ProbeProvider
Now func() time.Time
Proposals map[string]*domain.Proposal
@@ -49,7 +51,10 @@ func (s *Service) health(w http.ResponseWriter, _ *http.Request) {
}
type queueCreateRequest struct {
TicketID string `json:"ticket_id"`
TicketID string `json:"ticket_id"`
Playlist string `json:"playlist"`
ClientBuild string `json:"client_build"`
ProtocolVersion int `json:"protocol_version"`
}
type queueResponse struct {
TicketID string `json:"ticket_id"`
@@ -58,6 +63,7 @@ type queueResponse struct {
Revision uint64 `json:"revision"`
EnqueuedAt time.Time `json:"enqueued_at"`
ExpiresAt time.Time `json:"expires_at"`
Playlist string `json:"playlist"`
}
func (s *Service) queueCreate(w http.ResponseWriter, r *http.Request) {
@@ -69,7 +75,7 @@ func (s *Service) queueCreate(w http.ResponseWriter, r *http.Request) {
if !ok {
return
}
if s.Queue == nil || s.Candidate == nil {
if s.Queue == nil || (s.Candidate == nil && s.CandidateV2 == nil) {
writeError(w, http.StatusServiceUnavailable, "queue_unavailable")
return
}
@@ -77,7 +83,7 @@ func (s *Service) queueCreate(w http.ResponseWriter, r *http.Request) {
if !decodeBody(w, r, &input) {
return
}
if input.TicketID == "" {
if input.TicketID == "" || (input.Playlist != string(domain.Casual) && input.Playlist != string(domain.Ranked)) || input.ClientBuild == "" || len(input.ClientBuild) > 128 || input.ProtocolVersion < 1 {
writeError(w, http.StatusBadRequest, "invalid_request")
return
}
@@ -87,11 +93,27 @@ func (s *Service) queueCreate(w http.ResponseWriter, r *http.Request) {
return
}
now := s.now()
candidate, err := s.Candidate(playerID, input.TicketID)
spec := domain.QueueSpec{Playlist: domain.Playlist(input.Playlist), ClientBuild: input.ClientBuild, ProtocolVersion: input.ProtocolVersion}
var candidate domain.Candidate
var err error
if s.CandidateV2 != nil {
candidate, err = s.CandidateV2(playerID, input.TicketID, spec)
} else {
candidate, err = s.Candidate(playerID, input.TicketID)
// Legacy providers predate queue compatibility metadata. The API has
// validated the request; keep the resulting projection self-describing.
candidate.Playlist = spec.Playlist
candidate.ClientBuild = spec.ClientBuild
candidate.ProtocolVersion = spec.ProtocolVersion
}
if err != nil {
writeError(w, http.StatusUnprocessableEntity, "candidate_unavailable")
return
}
if candidate.PlayerID != playerID || candidate.TicketID != input.TicketID || candidate.Playlist != spec.Playlist || candidate.ClientBuild != spec.ClientBuild || candidate.ProtocolVersion != spec.ProtocolVersion {
writeError(w, http.StatusUnprocessableEntity, "candidate_mismatch")
return
}
ticket, err := s.Queue.Create(playerID, input.TicketID, key, candidate, now)
if err != nil {
writeDomainError(w, err)
@@ -322,7 +344,7 @@ func decodeBody(w http.ResponseWriter, r *http.Request, target any) bool {
}
func toQueueResponse(ticket domain.QueueTicket) queueResponse {
return queueResponse{TicketID: ticket.TicketID, PlayerID: ticket.PlayerID, State: string(ticket.State), Revision: ticket.Revision, EnqueuedAt: ticket.EnqueuedAt, ExpiresAt: ticket.ExpiresAt}
return queueResponse{TicketID: ticket.TicketID, PlayerID: ticket.PlayerID, Playlist: string(ticket.Playlist), State: string(ticket.State), Revision: ticket.Revision, EnqueuedAt: ticket.EnqueuedAt, ExpiresAt: ticket.ExpiresAt}
}
func toProposalResponse(proposal domain.Proposal) proposalResponse {