Files
story/internal/devicefleet/download.go
T
Codex Local bf1233d5df
ci / verify (push) Canceled after 0s
feat: expand companion platform, device controls, and memoir workflows
Add customer and operator login, BLE provisioning, device fleet controls, call memory, memoir generation, and refreshed family and maintenance portals. Include API contracts, migrations, firmware sources, tests, assets, and deployment documentation.
2026-10-08 10:06:44 +08:00

94 lines
2.9 KiB
Go

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)
}