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
70 changes: 70 additions & 0 deletions api/docs/docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -1941,6 +1941,10 @@ const docTemplate = `{
"name": {
"type": "string"
},
"playback_auth": {
"description": "PlaybackAuth overrides the global media-auth default policy for this\nstream: \"public\" (no token) or \"token\" (signed token required). Empty\ninherits auth.media.default_policy. Static rules (IP/country/UA/domains)\nremain global. See internal/mediaauth.",
"type": "string"
},
"protocols": {
"description": "Protocols defines which delivery protocols are opened for this stream.\nnil means the field is unset and ResolveStream inherits the template's\nProtocols (or leaves the resolved value nil when no template applies —\npublisher treats nil as \"no protocols enabled\"). An explicit non-nil\npointer — including the zero value \u0026OutputProtocols{} — is an\noperator-asserted override and beats template inheritance.",
"allOf": [
Expand Down Expand Up @@ -2104,6 +2108,9 @@ const docTemplate = `{
"properties": {
"api": {
"$ref": "#/definitions/config.APIAuthConfig"
},
"media": {
"$ref": "#/definitions/config.MediaAuthConfig"
}
}
},
Expand Down Expand Up @@ -2227,6 +2234,65 @@ const docTemplate = `{
}
}
},
"config.MediaAuthConfig": {
"type": "object",
"properties": {
"allow_countries": {
"type": "array",
"items": {
"type": "string"
}
},
"allow_ips": {
"description": "Static allow/deny chain. IPs accept exact addresses or CIDR ranges;\nCountries are ISO 3166-1 alpha-2 codes (need a GeoIP DB — see\nSessionsConfig.GeoIPDBPath); UserAgents match case-insensitive substring;\nAllowedDomains match the Referer host (exact or parent domain).",
"type": "array",
"items": {
"type": "string"
}
},
"allow_user_agents": {
"type": "array",
"items": {
"type": "string"
}
},
"allowed_domains": {
"type": "array",
"items": {
"type": "string"
}
},
"default_policy": {
"description": "DefaultPolicy is \"public\" (no token needed) or \"token\" (signed token\nrequired) for streams that don't set their own. Empty = public.",
"type": "string"
},
"deny_countries": {
"type": "array",
"items": {
"type": "string"
}
},
"deny_ips": {
"type": "array",
"items": {
"type": "string"
}
},
"deny_user_agents": {
"type": "array",
"items": {
"type": "string"
}
},
"enabled": {
"type": "boolean"
},
"token_secret": {
"description": "TokenSecret is the HMAC-SHA256 key the server uses to VERIFY playback\ntokens. Clients (your app) mint tokens with the same secret — the server\nnever issues them. Required when any stream's effective policy is \"token\".\nSee internal/mediaauth.SignToken for the canonical token format.",
"type": "string"
}
}
},
"config.PublisherConfig": {
"type": "object",
"properties": {
Expand Down Expand Up @@ -3242,6 +3308,10 @@ const docTemplate = `{
"description": "Name and Description are template-level metadata. Name surfaces in\nthe API for human-readable lists; Description carries the rationale\nbehind the template's settings. Streams inheriting this template\nkeep their own Name / Description fields — the template metadata is\nfor operator-facing tooling, not for downstream consumers.",
"type": "string"
},
"playback_auth": {
"description": "PlaybackAuth is the media-auth policy (\"public\"/\"token\") inherited by\nstreams referencing this template. Empty = inherit global default.",
"type": "string"
},
"prefixes": {
"description": "Prefixes is the list of URL-path prefixes that trigger auto-publish.\nWhen an encoder pushes to a path whose first segment(s) match any\nprefix here AND this template has at least one publish:// input, a\nruntime stream is created on the fly. Prefixes must not overlap any\nother template's prefix (validated at save time).",
"type": "array",
Expand Down
70 changes: 70 additions & 0 deletions api/docs/swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -1934,6 +1934,10 @@
"name": {
"type": "string"
},
"playback_auth": {
"description": "PlaybackAuth overrides the global media-auth default policy for this\nstream: \"public\" (no token) or \"token\" (signed token required). Empty\ninherits auth.media.default_policy. Static rules (IP/country/UA/domains)\nremain global. See internal/mediaauth.",
"type": "string"
},
"protocols": {
"description": "Protocols defines which delivery protocols are opened for this stream.\nnil means the field is unset and ResolveStream inherits the template's\nProtocols (or leaves the resolved value nil when no template applies —\npublisher treats nil as \"no protocols enabled\"). An explicit non-nil\npointer — including the zero value \u0026OutputProtocols{} — is an\noperator-asserted override and beats template inheritance.",
"allOf": [
Expand Down Expand Up @@ -2097,6 +2101,9 @@
"properties": {
"api": {
"$ref": "#/definitions/config.APIAuthConfig"
},
"media": {
"$ref": "#/definitions/config.MediaAuthConfig"
}
}
},
Expand Down Expand Up @@ -2220,6 +2227,65 @@
}
}
},
"config.MediaAuthConfig": {
"type": "object",
"properties": {
"allow_countries": {
"type": "array",
"items": {
"type": "string"
}
},
"allow_ips": {
"description": "Static allow/deny chain. IPs accept exact addresses or CIDR ranges;\nCountries are ISO 3166-1 alpha-2 codes (need a GeoIP DB — see\nSessionsConfig.GeoIPDBPath); UserAgents match case-insensitive substring;\nAllowedDomains match the Referer host (exact or parent domain).",
"type": "array",
"items": {
"type": "string"
}
},
"allow_user_agents": {
"type": "array",
"items": {
"type": "string"
}
},
"allowed_domains": {
"type": "array",
"items": {
"type": "string"
}
},
"default_policy": {
"description": "DefaultPolicy is \"public\" (no token needed) or \"token\" (signed token\nrequired) for streams that don't set their own. Empty = public.",
"type": "string"
},
"deny_countries": {
"type": "array",
"items": {
"type": "string"
}
},
"deny_ips": {
"type": "array",
"items": {
"type": "string"
}
},
"deny_user_agents": {
"type": "array",
"items": {
"type": "string"
}
},
"enabled": {
"type": "boolean"
},
"token_secret": {
"description": "TokenSecret is the HMAC-SHA256 key the server uses to VERIFY playback\ntokens. Clients (your app) mint tokens with the same secret — the server\nnever issues them. Required when any stream's effective policy is \"token\".\nSee internal/mediaauth.SignToken for the canonical token format.",
"type": "string"
}
}
},
"config.PublisherConfig": {
"type": "object",
"properties": {
Expand Down Expand Up @@ -3235,6 +3301,10 @@
"description": "Name and Description are template-level metadata. Name surfaces in\nthe API for human-readable lists; Description carries the rationale\nbehind the template's settings. Streams inheriting this template\nkeep their own Name / Description fields — the template metadata is\nfor operator-facing tooling, not for downstream consumers.",
"type": "string"
},
"playback_auth": {
"description": "PlaybackAuth is the media-auth policy (\"public\"/\"token\") inherited by\nstreams referencing this template. Empty = inherit global default.",
"type": "string"
},
"prefixes": {
"description": "Prefixes is the list of URL-path prefixes that trigger auto-publish.\nWhen an encoder pushes to a path whose first segment(s) match any\nprefix here AND this template has at least one publish:// input, a\nruntime stream is created on the fly. Prefixes must not overlap any\nother template's prefix (validated at save time).",
"type": "array",
Expand Down
64 changes: 64 additions & 0 deletions api/docs/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,13 @@ definitions:
type: array
name:
type: string
playback_auth:
description: |-
PlaybackAuth overrides the global media-auth default policy for this
stream: "public" (no token) or "token" (signed token required). Empty
inherits auth.media.default_policy. Static rules (IP/country/UA/domains)
remain global. See internal/mediaauth.
type: string
protocols:
allOf:
- $ref: '#/definitions/domain.OutputProtocols'
Expand Down Expand Up @@ -331,6 +338,8 @@ definitions:
properties:
api:
$ref: '#/definitions/config.APIAuthConfig'
media:
$ref: '#/definitions/config.MediaAuthConfig'
type: object
config.BufferConfig:
properties:
Expand Down Expand Up @@ -442,6 +451,56 @@ definitions:
healthy primary will be falsely failed over to a lower priority.
type: integer
type: object
config.MediaAuthConfig:
properties:
allow_countries:
items:
type: string
type: array
allow_ips:
description: |-
Static allow/deny chain. IPs accept exact addresses or CIDR ranges;
Countries are ISO 3166-1 alpha-2 codes (need a GeoIP DB — see
SessionsConfig.GeoIPDBPath); UserAgents match case-insensitive substring;
AllowedDomains match the Referer host (exact or parent domain).
items:
type: string
type: array
allow_user_agents:
items:
type: string
type: array
allowed_domains:
items:
type: string
type: array
default_policy:
description: |-
DefaultPolicy is "public" (no token needed) or "token" (signed token
required) for streams that don't set their own. Empty = public.
type: string
deny_countries:
items:
type: string
type: array
deny_ips:
items:
type: string
type: array
deny_user_agents:
items:
type: string
type: array
enabled:
type: boolean
token_secret:
description: |-
TokenSecret is the HMAC-SHA256 key the server uses to VERIFY playback
tokens. Clients (your app) mint tokens with the same secret — the server
never issues them. Required when any stream's effective policy is "token".
See internal/mediaauth.SignToken for the canonical token format.
type: string
type: object
config.PublisherConfig:
properties:
dash:
Expand Down Expand Up @@ -1405,6 +1464,11 @@ definitions:
keep their own Name / Description fields — the template metadata is
for operator-facing tooling, not for downstream consumers.
type: string
playback_auth:
description: |-
PlaybackAuth is the media-auth policy ("public"/"token") inherited by
streams referencing this template. Empty = inherit global default.
type: string
prefixes:
description: |-
Prefixes is the list of URL-path prefixes that trigger auto-publish.
Expand Down
49 changes: 49 additions & 0 deletions cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import (
"github.com/ntt0601zcoder/open-streamer/internal/hooks"
"github.com/ntt0601zcoder/open-streamer/internal/ingestor"
"github.com/ntt0601zcoder/open-streamer/internal/manager"
"github.com/ntt0601zcoder/open-streamer/internal/mediaauth"
"github.com/ntt0601zcoder/open-streamer/internal/metrics"
"github.com/ntt0601zcoder/open-streamer/internal/publisher"
"github.com/ntt0601zcoder/open-streamer/internal/runtime"
Expand Down Expand Up @@ -133,6 +134,11 @@ func run() error {
// 5. Wire all services.
wireServices(injector)

// 5b. Wire the media-plane playback authorizer (token / IP / country / UA /
// referer) into the publisher + API server. Returns the shared authorizer
// so the runtime config-reload path can hot-swap its rules.
mediaAuthz := wireMediaAuth(injector, gcfg)

// 6. Assemble RuntimeManager deps from DI.
rtm := runtime.New(ctx, runtime.Deps{
Ingestor: do.MustInvoke[*ingestor.Service](injector),
Expand All @@ -144,6 +150,7 @@ func run() error {
SessionsSvc: do.MustInvoke[*sessions.Service](injector),
AutoPublish: do.MustInvoke[*autopublish.Service](injector),
APISrv: do.MustInvoke[*api.Server](injector),
MediaAuth: mediaAuthz,
Bus: do.MustInvoke[events.Bus](injector),
StreamRepo: do.MustInvoke[store.StreamRepository](injector),
GlobalConfigRepo: gcRepo,
Expand Down Expand Up @@ -330,6 +337,48 @@ func wireServices(i *do.RootScope) {
// happens at packet-read time, long after the original request that created
// the worker has been served. The repo's FindByCode is fast (in-memory or
// indexed), so blocking briefly here is acceptable.
// wireMediaAuth builds the shared media-plane playback authorizer and injects
// it into the publisher (RTMP/SRT/RTSP play) and API server (HLS/DASH/MPEGTS).
// The country backend reuses the sessions GeoIP resolver; the per-stream policy
// is resolved from the publisher's in-memory stream table (O(1), no store hit).
func wireMediaAuth(i do.Injector, gcfg *domain.GlobalConfig) *mediaauth.Authorizer {
pub := do.MustInvoke[*publisher.Service](i)
apiSrv := do.MustInvoke[*api.Server](i)
geoIP := do.MustInvoke[*sessions.SwappableGeoIP](i)
streamRepo := do.MustInvoke[store.StreamRepository](i)
templateRepo := do.MustInvoke[store.TemplateRepository](i)

// policyFor resolves a stream's effective playback policy. Live streams hit
// the publisher's in-memory table (O(1), no store read on the hot path). A
// STOPPED stream — whose DVR archive is still served — falls back to the
// store + template so its per-stream `token` policy isn't silently
// downgraded to the global default.
policyFor := func(code domain.StreamCode) string {
if policy, running := pub.PlaybackPolicy(code); running {
return policy
}
s, err := streamRepo.FindByCode(context.Background(), code)
if err != nil {
return ""
}
if s.Template != nil {
if tpl, terr := templateRepo.FindByCode(context.Background(), *s.Template); terr == nil {
s = domain.ResolveStream(s, tpl)
}
}
return s.PlaybackAuth
}

cfg := config.AuthConfig{}
if gcfg.Auth != nil {
cfg = *gcfg.Auth
}
authz := mediaauth.New(cfg.Media, geoIP.Country, policyFor)
pub.SetMediaAuthorizer(authz)
apiSrv.SetMediaAuthorizer(authz)
return authz
}

func wireCopyLookup(i do.Injector) {
ing := do.MustInvoke[*ingestor.Service](i)
repo := do.MustInvoke[store.StreamRepository](i)
Expand Down
Loading