Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 11 additions & 56 deletions backend/cmd/api/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import (
"github.com/bcc-code/bcc-media-platform/backend/search"

"github.com/bcc-code/bcc-media-platform/backend/auth0"
"github.com/bcc-code/bcc-media-platform/backend/streamtoken"
"github.com/joho/godotenv"
"github.com/samber/lo"
)
Expand Down Expand Up @@ -95,7 +94,6 @@ type envConfig struct {
Port string
Auth0 auth0.Config
CDNConfig cdnConfig
Livestream livestreamConfig
StreamProxy streamProxyConfig
Secrets serviceSecrets
Redis utils.RedisConfig
Expand Down Expand Up @@ -130,54 +128,22 @@ type adminConfig struct {
type cdnConfig struct {
ImageCDNDomain string
Vod2Domain string
LegacyVODDomain string
FilesDomain string
AWSSigningKeyPath string
AWSSigningKeyID string
}

// livestreamConfig holds the CloudFront key pair used to sign the livestream
// manifest URL. This is a separate key pair from the VOD/file signing key in
// cdnConfig. It satisfies signing.CloudFrontConfig.
type livestreamConfig struct {
SigningKeyPath string
SigningKeyID string
}

func (c livestreamConfig) GetAwsSigningKeyPath() string { return c.SigningKeyPath }
func (c livestreamConfig) GetAwsSigningKeyID() string { return c.SigningKeyID }

// streamProxyConfig is what the API needs to mint stream-proxy URLs (HS256 JWT
// on the proxy's public host). It satisfies streamtoken.Config.
type streamProxyConfig struct {
JWTSecret string
JWTIssuer string
Domain string
PrimaryProvider streamtoken.Provider
JWTSecret string
JWTIssuer string
Domain string
}

func (c streamProxyConfig) GetStreamJWTSecret() string { return c.JWTSecret }
func (c streamProxyConfig) GetStreamJWTIssuer() string { return c.JWTIssuer }
func (c streamProxyConfig) GetStreamProxyDomain() string { return c.Domain }
func (c streamProxyConfig) GetStreamPrimaryProvider() streamtoken.Provider { return c.PrimaryProvider }

// parsePrimaryProvider maps the user-facing STREAM_PRIMARY_PROVIDER values
// ("cloudfront", "streamproxy") to the streamtoken.Provider used as the JWT
// `provider` claim. The claim values are different ("cloudfront", "ioriver")
// because they are the stream-proxy's internal vocabulary; the API exposes
// the same routing decision under a name that abstracts away the
// underlying signer technology. Empty raw → ProviderUnspecified (signer
// substitutes its own default). Unknown raw passes through so the signer's
// validity check rejects it with a clear error.
func parsePrimaryProvider(raw string) streamtoken.Provider {
switch raw {
case "":
return streamtoken.ProviderUnspecified
case "cloudfront":
return streamtoken.ProviderCloudFront
case "streamproxy":
return streamtoken.ProviderIoriver
}
return streamtoken.Provider(raw)
}
func (c streamProxyConfig) GetStreamJWTSecret() string { return c.JWTSecret }
func (c streamProxyConfig) GetStreamJWTIssuer() string { return c.JWTIssuer }
func (c streamProxyConfig) GetStreamProxyDomain() string { return c.Domain }

type awsConfig struct {
TempBucket string // Things put here are automatically removed
Expand Down Expand Up @@ -235,11 +201,6 @@ func (c cdnConfig) GetVOD2Domain() string {
return c.Vod2Domain
}

// GetLegacyVODDomain returns the legacy VOD domain
func (c cdnConfig) GetLegacyVODDomain() string {
return c.LegacyVODDomain
}

// GetFilesCDNDomain returns the configured FilesCDNDomain
func (c cdnConfig) GetFilesCDNDomain() string {
return c.FilesDomain
Expand Down Expand Up @@ -316,17 +277,11 @@ func getEnvConfig() envConfig {
FilesDomain: os.Getenv("FILES_CDN_DOMAIN"),
AWSSigningKeyID: os.Getenv("CF_SIGNING_KEY_ID"),
AWSSigningKeyPath: os.Getenv("CF_SIGNING_KEY_PATH"),
LegacyVODDomain: os.Getenv("LEGACY_CDN_DOMAIN"),
},
Livestream: livestreamConfig{
SigningKeyID: os.Getenv("LIVESTREAM_SIGNING_KEY_ID"),
SigningKeyPath: os.Getenv("LIVESTREAM_SIGNING_KEY_PATH"),
},
StreamProxy: streamProxyConfig{
JWTSecret: os.Getenv("STREAM_JWT_SECRET"),
JWTIssuer: os.Getenv("STREAM_JWT_ISSUER"),
Domain: os.Getenv("STREAM_PROXY_DOMAIN"),
PrimaryProvider: parsePrimaryProvider(os.Getenv("STREAM_PRIMARY_PROVIDER")),
JWTSecret: os.Getenv("STREAM_JWT_SECRET"),
JWTIssuer: os.Getenv("STREAM_JWT_ISSUER"),
Domain: os.Getenv("STREAM_PROXY_DOMAIN"),
},
Secrets: serviceSecrets{
Directus: os.Getenv("SERVICE_SECRET_DIRECTUS"),
Expand Down
15 changes: 5 additions & 10 deletions backend/cmd/api/env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,6 @@ IMAGE_CDN_DOMAIN=brunstadtv.imgix.net
VOD2_CDN_DOMAIN=vod2.brunstad.tv
FILES_CDN_DOMAIN=files.brunstad.tv
CF_SIGNING_KEY_ID=<SIGNING_KEY_ID>
LIVESTREAM_SIGNING_KEY_ID=<LIVESTREAM_SIGNING_KEY_ID>
LIVESTREAM_SIGNING_KEY_PATH=<PATH TO LIVESTREAM PEM KEY>
AZ_SIGNING_KEY=<BASE 64 ENCODED SIMMETRYC KEY>

# Stream proxy: HMAC secret + issuer the API uses to mint stream JWTs.
Expand All @@ -32,14 +30,11 @@ STREAM_JWT_ISSUER=
# Public hostname clients use to reach the stream proxy.
# For local dev, point at the locally-running cmd/stream-proxy (default port 8081).
STREAM_PROXY_DOMAIN=localhost:8081
# Optional. Selects how the API delivers stream URLs to clients:
# "cloudfront" (or empty) → API signs CloudFront URLs directly; the
# stream-proxy is NOT in the request path.
# "streamproxy" → API returns a stream-proxy URL with an HS256
# JWT; the proxy signs upstream to ioriver.
# The Unleash `stream-proxy:legacy` flag, when set on a request, overrides
# this and forces the CloudFront-direct signer for that request only.
STREAM_PRIMARY_PROVIDER=
# All stream URLs (VOD and live) are stream-proxy URLs. Which upstream CDN
# identity the proxy signs for is the JWT `provider` claim: ioriver by default;
# the Unleash `cdn-provider` (VOD) / `live-cdn-provider` (live) variants
# `ioriver` / `cloudfront`, forwarded by clients in x-feature-flags, override it
# per request.

# Unleash usage reporting. The clients evaluate flags and forward them to us as
# the x-feature-flags header, so the API runs no SDK — it only posts metrics
Expand Down
5 changes: 0 additions & 5 deletions backend/cmd/api/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,6 @@ func graphqlHandler(
emailService *email.Service,
fileSigner *signing.CloudFrontSigner,
streamSigner *streamtoken.Signer,
legacyStreamSigner *signing.CloudFrontStreamSigner,
livestreamSigner *signing.CloudFrontSigner,
config envConfig,
s3client *s3.Client,
analyticsSalt string,
Expand All @@ -71,9 +69,6 @@ func graphqlHandler(
EmailService: emailService,
FileSigner: fileSigner,
StreamURLSigner: streamSigner,
LegacyStreamSigner: legacyStreamSigner,
LivestreamSigner: livestreamSigner,
PrimaryStreamProvider: config.StreamProxy.PrimaryProvider,
S3Client: s3client,
APIConfig: config.CDNConfig,
AWSConfig: config.AWS,
Expand Down
15 changes: 0 additions & 15 deletions backend/cmd/api/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -195,23 +195,10 @@ func main() {
if err != nil {
log.L.Panic().Err(err).Msg("failed to init cloudfront file signer")
}
legacyStreamSigner := signing.NewCloudFrontStreamSigner(fileSigner, config.CDNConfig.GetVOD2Domain())
streamSigner, err := streamtoken.NewSigner(config.StreamProxy)
if err != nil {
log.L.Panic().Err(err).Msg("failed to init stream-proxy signer")
}
// The livestream uses its own CloudFront key pair. Optional: when it isn't
// configured the API still boots and live.isOnline works, but live.url is
// omitted (rather than panicking environments without the key deployed).
var livestreamSigner *signing.CloudFrontSigner
if config.Livestream.GetAwsSigningKeyPath() != "" {
livestreamSigner, err = signing.NewCloudFrontSigner(config.Livestream)
if err != nil {
log.L.Panic().Err(err).Msg("failed to init livestream signer")
}
} else {
log.L.Warn().Msg("livestream signing key not configured (LIVESTREAM_SIGNING_KEY_PATH); live.url will be unavailable")
}
queries := sqlc.New(db)
queries.SetImageCDNDomain(config.CDNConfig.ImageCDNDomain)
authClient := auth0.New(config.Auth0)
Expand Down Expand Up @@ -343,8 +330,6 @@ func main() {
emailService,
fileSigner,
streamSigner,
legacyStreamSigner,
livestreamSigner,
config,
s3Client,
config.AnalyticsSalt,
Expand Down
18 changes: 18 additions & 0 deletions backend/cmd/jobs/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,18 @@ func (c cdnConfig) GetAwsSigningKeyID() string {
return c.AWSSigningKeyID
}

// streamProxyConfig is what the export needs to mint stream-proxy URLs. The
// values MUST match cmd/api and cmd/stream-proxy. It satisfies streamtoken.Config.
type streamProxyConfig struct {
JWTSecret string
JWTIssuer string
Domain string
}

func (c streamProxyConfig) GetStreamJWTSecret() string { return c.JWTSecret }
func (c streamProxyConfig) GetStreamJWTIssuer() string { return c.JWTIssuer }
func (c streamProxyConfig) GetStreamProxyDomain() string { return c.Domain }

type envConfig struct {
AWS awsConfig
AzureStorage files.AzureConfig
Expand All @@ -76,6 +88,7 @@ type envConfig struct {
VideoManipulator videomanipulatorConfig
Phrase phrase.Config
CDNConfig cdnConfig
StreamProxy streamProxyConfig
}

func getEnvConfig() envConfig {
Expand Down Expand Up @@ -173,5 +186,10 @@ func getEnvConfig() envConfig {
AWSSigningKeyID: os.Getenv("CF_SIGNING_KEY_ID"),
AWSSigningKeyPath: os.Getenv("CF_SIGNING_KEY_PATH"),
},
StreamProxy: streamProxyConfig{
JWTSecret: os.Getenv("STREAM_JWT_SECRET"),
JWTIssuer: os.Getenv("STREAM_JWT_ISSUER"),
Domain: os.Getenv("STREAM_PROXY_DOMAIN"),
},
}
}
9 changes: 7 additions & 2 deletions backend/cmd/jobs/env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,13 @@ AZURE_STORAGE_CONTAINER=images
VIDEOMANIPULATOR_BASE_URL=http://localhost:8005/
VIDEOMANIPULATOR_API_KEY=

# CDN signing (CloudFront, used for downloadable file URLs and the legacy
# stream-URL form embedded in offline-export bundles).
# CDN signing (CloudFront, used for downloadable file URLs).
VOD2_CDN_DOMAIN=
CF_SIGNING_KEY_ID=
CF_SIGNING_KEY_PATH=

# Stream proxy: HMAC secret + issuer + public host used to mint the stream URLs
# embedded in offline-export bundles. These MUST match cmd/api and cmd/stream-proxy.
STREAM_JWT_SECRET=
STREAM_JWT_ISSUER=
STREAM_PROXY_DOMAIN=
8 changes: 6 additions & 2 deletions backend/cmd/jobs/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"github.com/bcc-code/bcc-media-platform/backend/signing"
"github.com/bcc-code/bcc-media-platform/backend/sqlc"
"github.com/bcc-code/bcc-media-platform/backend/statistics"
"github.com/bcc-code/bcc-media-platform/backend/streamtoken"
"github.com/bcc-code/bcc-media-platform/backend/translations"
"github.com/bcc-code/bcc-media-platform/backend/translations/phrase"
"github.com/bcc-code/bcc-media-platform/backend/utils"
Expand Down Expand Up @@ -150,7 +151,10 @@ func main() {
if err != nil {
log.L.Fatal().Err(err).Msg("failed to init cloudfront file signer")
}
legacyStreamSigner := signing.NewCloudFrontStreamSigner(fileSigner, config.CDNConfig.GetVOD2Domain())
streamSigner, err := streamtoken.NewSigner(config.StreamProxy)
if err != nil {
log.L.Fatal().Err(err).Msg("failed to init stream-proxy signer")
}

services := server.ExternalServices{
Database: db,
Expand All @@ -168,7 +172,7 @@ func main() {
CDNConfigProvider: config.CDNConfig,
BatchLoaders: loaders.InitBatchLoaders(queries, nil),
FileSigner: fileSigner,
LegacyStreamSigner: legacyStreamSigner,
StreamSigner: streamSigner,
}

handlers := server.NewServer(services, serverConfig)
Expand Down
9 changes: 6 additions & 3 deletions backend/cmd/jobs/server/services.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ package server

import (
"database/sql"

"github.com/bcc-code/bcc-media-platform/backend/common"
"github.com/bcc-code/bcc-media-platform/backend/export"
"github.com/bcc-code/bcc-media-platform/backend/loaders"
"github.com/bcc-code/bcc-media-platform/backend/signing"
"github.com/bcc-code/bcc-media-platform/backend/streamtoken"
"github.com/bcc-code/bcc-media-platform/backend/translations"

"github.com/aws/aws-sdk-go-v2/service/mediapackagevod"
Expand Down Expand Up @@ -38,7 +40,7 @@ type ExternalServices struct {
CDNConfigProvider export.CDNConfig
BatchLoaders *loaders.BatchLoaders
FileSigner *signing.CloudFrontSigner
LegacyStreamSigner *signing.CloudFrontStreamSigner
StreamSigner *streamtoken.Signer
}

// GetDatabase as stored in the struct
Expand Down Expand Up @@ -112,6 +114,7 @@ func (e ExternalServices) GetFileSigner() *signing.CloudFrontSigner {
return e.FileSigner
}

func (e ExternalServices) GetLegacyStreamSigner() *signing.CloudFrontStreamSigner {
return e.LegacyStreamSigner
// GetStreamSigner returns the stream-proxy URL signer used by the export.
func (e ExternalServices) GetStreamSigner() *streamtoken.Signer {
return e.StreamSigner
}
19 changes: 9 additions & 10 deletions backend/export/export.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import (

"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/bcc-code/bcc-media-platform/backend/graph/api/model"
"github.com/bcc-code/bcc-media-platform/backend/signing"
"github.com/bcc-code/bcc-media-platform/backend/utils"
"github.com/cloudevents/sdk-go/v2/event"
"github.com/google/uuid"
Expand Down Expand Up @@ -68,7 +67,7 @@ type serviceProvider interface {
GetLoadersForRoles(roles []string) *loaders.LoadersWithPermissions
GetPersonalizedLoaders(roles []string, langPreferences common.LanguagePreferences) *loaders.PersonalizedLoaders
GetS3Client() *s3.Client
GetLegacyStreamSigner() *signing.CloudFrontStreamSigner
GetStreamSigner() *streamtoken.Signer
GetCDNConfig() CDNConfig
}

Expand All @@ -79,7 +78,7 @@ type serviceProviderAPI interface {
GetFilteredLoaders(ctx context.Context) *loaders.LoadersWithPermissions
GetPersonalizedLoaders(ctx context.Context) *loaders.PersonalizedLoaders
GetQueries() *sqlc.Queries
GetLegacyStreamSigner() *signing.CloudFrontStreamSigner
GetStreamSigner() *streamtoken.Signer
GetS3Client() *s3.Client
}

Expand Down Expand Up @@ -239,10 +238,10 @@ func exportEpisodes(ctx context.Context, batchLoaders *loaders.BatchLoaders, fil
}

// exportStreams writes per-episode stream URLs into the offline-export
// SQLite db. Exports always use the legacy CloudFront-signed URL form because
// jobs run without a request context to evaluate the per-user feature flag
// that switches between the proxy and legacy paths.
func exportStreams(ctx context.Context, ls *loaders.BatchLoaders, streamSigner *signing.CloudFrontStreamSigner, liteQueries *sqlexport.Queries, episodeIDs []int) error {
// SQLite db as stream-proxy URLs. Exports run without a request context to
// evaluate the per-user `cdn-provider` flag, so they always use the signer's
// default upstream identity (streamtoken.DefaultPrimaryProvider).
func exportStreams(ctx context.Context, ls *loaders.BatchLoaders, streamSigner *streamtoken.Signer, liteQueries *sqlexport.Queries, episodeIDs []int) error {

episodes, err := ls.EpisodeLoader.GetMany(ctx, episodeIDs)
if err != nil {
Expand Down Expand Up @@ -512,7 +511,7 @@ func HandleExportMessage(ctx context.Context, s serviceProvider, tempBucketNeme
s.GetLoaders(),
s.GetLoadersForRoles(fetchedEntry.UserGroups),
s.GetPersonalizedLoaders(fetchedEntry.UserGroups, langPreferences),
s.GetLegacyStreamSigner(),
s.GetStreamSigner(),
s.GetS3Client(),
s.GetDatabase(),
)
Expand Down Expand Up @@ -578,7 +577,7 @@ func DoExport(ctx context.Context, q serviceProviderAPI, bucketName string, user
q.GetLoaders(),
q.GetFilteredLoaders(ctx),
q.GetPersonalizedLoaders(ctx),
q.GetLegacyStreamSigner(),
q.GetStreamSigner(),
q.GetS3Client(),
q.GetDatabase(),
)
Expand All @@ -593,7 +592,7 @@ func doExport(
batchLoaders *loaders.BatchLoaders,
roleLoaders *loaders.LoadersWithPermissions,
personalizedLoaders *loaders.PersonalizedLoaders,
streamSigner *signing.CloudFrontStreamSigner,
streamSigner *streamtoken.Signer,
s3Client *s3.Client,
pgSql *sql.DB,
) (string, error) {
Expand Down
Loading