package devicefleet import ( "context" "crypto/rand" "crypto/sha256" "encoding/base64" "errors" "io" "net/url" "strings" "time" "github.com/jackc/pgx/v5" ) const ( downloadTokenBytes = 16 downloadTicketTTL = 30 * time.Minute ) var ErrDownloadTicketUnavailable = errors.New("firmware download ticket is unavailable") type DownloadGrant struct { ObjectRef string Size int64 SHA256 string CreatedAt time.Time ExpiresAt time.Time } func newDownloadTicket(baseURL string) (string, []byte, string, time.Time, error) { raw := make([]byte, downloadTokenBytes) if _, err := rand.Read(raw); err != nil { return "", nil, "", time.Time{}, err } token := base64.RawURLEncoding.EncodeToString(raw) digest := sha256.Sum256([]byte(token)) address, err := shortDownloadURL(baseURL, token) if err != nil { return "", nil, "", time.Time{}, err } return token, digest[:], address, time.Now().UTC().Add(downloadTicketTTL), nil } func shortDownloadURL(baseURL, token string) (string, error) { parsed, err := url.Parse(strings.TrimSpace(baseURL)) if err != nil || parsed.Host == "" || parsed.User != nil || (parsed.Scheme != "https" && parsed.Scheme != "http") || (parsed.Path != "" && parsed.Path != "/") || parsed.RawQuery != "" || parsed.Fragment != "" { return "", errors.New("firmware download base URL must be an HTTP(S) origin") } parsed.Path = "/f/" + token return parsed.String(), nil } func downloadTokenHash(token string) ([]byte, error) { raw, err := base64.RawURLEncoding.DecodeString(token) if err != nil || len(raw) != downloadTokenBytes || base64.RawURLEncoding.EncodeToString(raw) != token { return nil, ErrDownloadTicketUnavailable } digest := sha256.Sum256([]byte(token)) return digest[:], nil } func (s *Service) ResolveDownload(ctx context.Context, token string) (DownloadGrant, error) { var grant DownloadGrant digest, err := downloadTokenHash(strings.TrimSpace(token)) if err != nil { return grant, err } err = s.Pool.QueryRow(ctx, ` SELECT f.object_ref,f.size,f.sha256,f.created_at,t.expires_at FROM fleet_firmware_download_ticket t JOIN fleet_firmware f ON f.id=t.release_id LEFT JOIN fleet_ota o ON o.id=t.ota_id WHERE t.token_hash=$1 AND t.revoked_at IS NULL AND t.expires_at>now() AND (t.ota_id IS NULL OR (o.status='pending' AND o.created_at>now()-interval '30 seconds') OR (o.status='accepted' AND o.created_at>now()-interval '20 minutes')) AND f.status='READY'`, digest).Scan(&grant.ObjectRef, &grant.Size, &grant.SHA256, &grant.CreatedAt, &grant.ExpiresAt) if errors.Is(err, pgx.ErrNoRows) { return DownloadGrant{}, ErrDownloadTicketUnavailable } return grant, err } func (s *Service) OpenDownload(ctx context.Context, grant DownloadGrant) (io.ReadSeekCloser, error) { if s.Store == nil || grant.ObjectRef == "" { return nil, ErrDownloadTicketUnavailable } return s.Store.OpenDownload(ctx, grant.ObjectRef) }