Files

94 lines
2.9 KiB
Go
Raw Permalink Normal View History

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