package service import ( "context" "strings" "google.golang.org/grpc/codes" "google.golang.org/grpc/metadata" "google.golang.org/grpc/status" statsv1 "github.com/bergabruh/stats-gateway/gen/stats/v1" "github.com/bergabruh/stats-gateway/internal/storage" ) // RecordSpeedTest ingests a single client-side speedtest sample from an edge // node. Authenticates via Bearer JWT (same scheme as Tunnel), validates the // request, and persists via the storage layer. func (s *Service) RecordSpeedTest(ctx context.Context, req *statsv1.RecordSpeedTestRequest) (*statsv1.RecordSpeedTestResponse, error) { // 1. JWT auth — mirrors the Tunnel handler. md, ok := metadata.FromIncomingContext(ctx) if !ok { return nil, status.Error(codes.Unauthenticated, "no metadata") } authH := md.Get("authorization") if len(authH) == 0 { return nil, status.Error(codes.Unauthenticated, "no auth") } tok := strings.TrimPrefix(authH[0], "Bearer ") if tok == authH[0] { return nil, status.Error(codes.Unauthenticated, "expected Bearer") } if _, err := s.verifier.Verify(tok); err != nil { return nil, status.Errorf(codes.Unauthenticated, "auth: %v", err) } // 2. Input validation. if req.Node != "fl" && req.Node != "pl1" { return nil, status.Errorf(codes.InvalidArgument, "node: must be 'fl' or 'pl1', got %q", req.Node) } if !validCountry(req.Country) { return nil, status.Errorf(codes.InvalidArgument, "country: must match ^[A-Z]{2}$ or 'XX', got %q", req.Country) } if req.DlMbps < 0 { return nil, status.Errorf(codes.InvalidArgument, "dl_mbps: must be >= 0, got %v", req.DlMbps) } if req.UlMbps < 0 { return nil, status.Errorf(codes.InvalidArgument, "ul_mbps: must be >= 0, got %v", req.UlMbps) } if req.PingMs < 0 { return nil, status.Errorf(codes.InvalidArgument, "ping_ms: must be >= 0, got %v", req.PingMs) } if req.JitterMs < 0 { return nil, status.Errorf(codes.InvalidArgument, "jitter_ms: must be >= 0, got %v", req.JitterMs) } if req.Timestamp <= 0 { return nil, status.Errorf(codes.InvalidArgument, "timestamp: must be > 0, got %d", req.Timestamp) } // 3. Persist. id, err := s.store.SaveSpeedTest(ctx, storage.SpeedTest{ Timestamp: req.Timestamp, Node: req.Node, Country: req.Country, DlMbps: req.DlMbps, UlMbps: req.UlMbps, PingMs: req.PingMs, JitterMs: req.JitterMs, }) if err != nil { return nil, status.Errorf(codes.Internal, "save: %v", err) } return &statsv1.RecordSpeedTestResponse{Id: id}, nil } // validCountry returns true when c is two uppercase ASCII letters (ISO-3166-1 // alpha-2 shape) or the literal "XX" sentinel for unknown. func validCountry(c string) bool { if len(c) != 2 { return false } if c == "XX" { return true } for i := 0; i < 2; i++ { if c[i] < 'A' || c[i] > 'Z' { return false } } return true }