WIP: verify policies for processes, rtmp, and srt. rewriter for local urls

This commit is contained in:
Ingo Oppermann
2023-02-15 21:43:37 +01:00
parent 74d0a96e1c
commit acc4d7ec5a
49 changed files with 2035 additions and 1827 deletions

View File

@@ -34,6 +34,7 @@ import (
"github.com/datarhei/core/v16/restream" "github.com/datarhei/core/v16/restream"
restreamapp "github.com/datarhei/core/v16/restream/app" restreamapp "github.com/datarhei/core/v16/restream/app"
"github.com/datarhei/core/v16/restream/replace" "github.com/datarhei/core/v16/restream/replace"
"github.com/datarhei/core/v16/restream/rewrite"
restreamstore "github.com/datarhei/core/v16/restream/store" restreamstore "github.com/datarhei/core/v16/restream/store"
"github.com/datarhei/core/v16/rtmp" "github.com/datarhei/core/v16/rtmp"
"github.com/datarhei/core/v16/service" "github.com/datarhei/core/v16/service"
@@ -440,6 +441,9 @@ func (a *api) start() error {
return fmt.Errorf("iam: %w", err) return fmt.Errorf("iam: %w", err)
} }
// Create default policies for anonymous users in order to mimic
// the behaviour before IAM
iam.RemovePolicy("$anon", "$none", "", "") iam.RemovePolicy("$anon", "$none", "", "")
iam.RemovePolicy("$localhost", "$none", "", "") iam.RemovePolicy("$localhost", "$none", "", "")
@@ -465,6 +469,14 @@ func (a *api) start() error {
iam.AddPolicy("$localhost", "$none", "fs:/memfs/**", "GET|HEAD|OPTIONS|POST|PUT|DELETE") iam.AddPolicy("$localhost", "$none", "fs:/memfs/**", "GET|HEAD|OPTIONS|POST|PUT|DELETE")
} }
if cfg.RTMP.Enable && len(cfg.RTMP.Token) == 0 {
iam.AddPolicy("$anon", "$none", "rtmp:/**", "PUBLISH|PLAY")
}
if cfg.SRT.Enable && len(cfg.SRT.Token) == 0 {
iam.AddPolicy("$anon", "$none", "srt:**", "PUBLISH|PLAY")
}
a.iam = iam a.iam = iam
} }
@@ -592,6 +604,35 @@ func (a *api) start() error {
a.ffmpeg = ffmpeg a.ffmpeg = ffmpeg
var rw rewrite.Rewriter
{
baseAddress := func(address string) string {
var base string
host, port, _ := gonet.SplitHostPort(address)
if len(host) == 0 {
base = "localhost:" + port
} else {
base = address
}
return base
}
httpBase := baseAddress(cfg.Address)
rtmpBase := baseAddress(cfg.RTMP.Address) + cfg.RTMP.App
srtBase := baseAddress(cfg.SRT.Address)
rw, err = rewrite.New(rewrite.Config{
HTTPBase: "http://" + httpBase,
RTMPBase: "rtmp://" + rtmpBase,
SRTBase: "srt://" + srtBase,
})
if err != nil {
return fmt.Errorf("unable to create url rewriter: %w", err)
}
}
a.replacer = replace.New() a.replacer = replace.New()
{ {
@@ -627,8 +668,8 @@ func (a *api) start() error {
} }
template += "/{name}" template += "/{name}"
if len(cfg.RTMP.Token) != 0 { if identity, _ := a.iam.GetIdentity(config.Owner); identity != nil {
template += "?token=" + cfg.RTMP.Token template += "/" + identity.GetServiceToken()
} }
return template return template
@@ -643,14 +684,14 @@ func (a *api) start() error {
template := "srt://" + host + ":" + port + "?mode=caller&transtype=live&latency={latency}&streamid={name}" template := "srt://" + host + ":" + port + "?mode=caller&transtype=live&latency={latency}&streamid={name}"
if section == "output" { if section == "output" {
template += ",mode:publish" template += ",mode:publish"
} else {
template += ",mode:request"
} }
if len(cfg.SRT.Token) != 0 {
template += ",token:" + cfg.SRT.Token if identity, _ := a.iam.GetIdentity(config.Owner); identity != nil {
template += ",token:" + identity.GetServiceToken()
} }
if len(cfg.SRT.Passphrase) != 0 { if len(cfg.SRT.Passphrase) != 0 {
template += "&passphrase=" + cfg.SRT.Passphrase template += "&passphrase=" + url.QueryEscape(cfg.SRT.Passphrase)
} }
return template return template
@@ -693,8 +734,10 @@ func (a *api) start() error {
Store: store, Store: store,
Filesystems: filesystems, Filesystems: filesystems,
Replace: a.replacer, Replace: a.replacer,
Rewrite: rw,
FFmpeg: a.ffmpeg, FFmpeg: a.ffmpeg,
MaxProcesses: cfg.FFmpeg.MaxProcesses, MaxProcesses: cfg.FFmpeg.MaxProcesses,
IAM: a.iam,
Logger: a.log.logger.core.WithComponent("Process"), Logger: a.log.logger.core.WithComponent("Process"),
}) })
@@ -703,49 +746,7 @@ func (a *api) start() error {
} }
a.restream = restream a.restream = restream
/*
var httpjwt jwt.JWT
if cfg.API.Auth.Enable {
secret := rand.String(32)
if len(cfg.API.Auth.JWT.Secret) != 0 {
secret = cfg.API.Auth.Username + cfg.API.Auth.Password + cfg.API.Auth.JWT.Secret
}
var err error
httpjwt, err = jwt.New(jwt.Config{
Realm: app.Name,
Secret: secret,
SkipLocalhost: cfg.API.Auth.DisableLocalhost,
})
if err != nil {
return fmt.Errorf("unable to create JWT provider: %w", err)
}
if validator, err := jwt.NewLocalValidator(a.iam); err == nil {
if err := httpjwt.AddValidator(app.Name, validator); err != nil {
return fmt.Errorf("unable to add local JWT validator: %w", err)
}
} else {
return fmt.Errorf("unable to create local JWT validator: %w", err)
}
if cfg.API.Auth.Auth0.Enable {
for _, t := range cfg.API.Auth.Auth0.Tenants {
if validator, err := jwt.NewAuth0Validator(a.iam); err == nil {
if err := httpjwt.AddValidator("https://"+t.Domain+"/", validator); err != nil {
return fmt.Errorf("unable to add Auth0 JWT validator: %w", err)
}
} else {
return fmt.Errorf("unable to create Auth0 JWT validator: %w", err)
}
}
}
}
a.httpjwt = httpjwt
*/
metrics, err := monitor.NewHistory(monitor.HistoryConfig{ metrics, err := monitor.NewHistory(monitor.HistoryConfig{
Enable: cfg.Metrics.Enable, Enable: cfg.Metrics.Enable,
Timerange: time.Duration(cfg.Metrics.Range) * time.Second, Timerange: time.Duration(cfg.Metrics.Range) * time.Second,

View File

@@ -1,502 +0,0 @@
package main
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"sync"
"github.com/casbin/casbin/v2/model"
"github.com/casbin/casbin/v2/persist"
)
// Adapter is the file adapter for Casbin.
// It can load policy from file or save policy to file.
type adapter struct {
filePath string
groups []Group
lock sync.Mutex
}
func NewAdapter(filePath string) persist.Adapter {
return &adapter{filePath: filePath}
}
// Adapter
func (a *adapter) LoadPolicy(model model.Model) error {
a.lock.Lock()
defer a.lock.Unlock()
if a.filePath == "" {
return fmt.Errorf("invalid file path, file path cannot be empty")
}
/*
logger := &log.DefaultLogger{}
logger.EnableLog(true)
model.SetLogger(logger)
*/
return a.loadPolicyFile(model)
}
func (a *adapter) loadPolicyFile(model model.Model) error {
if _, err := os.Stat(a.filePath); os.IsNotExist(err) {
a.groups = []Group{}
return nil
}
data, err := os.ReadFile(a.filePath)
if err != nil {
return err
}
groups := []Group{}
err = json.Unmarshal(data, &groups)
if err != nil {
return err
}
rule := [5]string{}
for _, group := range groups {
rule[0] = "p"
rule[2] = group.Name
for name, roles := range group.Roles {
rule[1] = "role:" + name
for _, role := range roles {
rule[3] = role.Resource
rule[4] = role.Actions
if err := a.importPolicy(model, rule[0:5]); err != nil {
return err
}
}
}
for _, policy := range group.Policies {
rule[1] = policy.Username
rule[3] = policy.Resource
rule[4] = policy.Actions
if err := a.importPolicy(model, rule[0:5]); err != nil {
return err
}
}
rule[0] = "g"
rule[3] = group.Name
for _, ug := range group.UserRoles {
rule[1] = ug.Username
rule[2] = "role:" + ug.Role
if err := a.importPolicy(model, rule[0:4]); err != nil {
return err
}
}
}
a.groups = groups
return nil
}
func (a *adapter) importPolicy(model model.Model, rule []string) error {
copiedRule := make([]string, len(rule))
copy(copiedRule, rule)
ok, err := model.HasPolicyEx(copiedRule[0], copiedRule[0], copiedRule[1:])
if err != nil {
return err
}
if ok {
return nil // skip duplicated policy
}
model.AddPolicy(copiedRule[0], copiedRule[0], copiedRule[1:])
return nil
}
// Adapter
func (a *adapter) SavePolicy(model model.Model) error {
a.lock.Lock()
defer a.lock.Unlock()
return a.savePolicyFile()
}
func (a *adapter) savePolicyFile() error {
if a.filePath == "" {
return fmt.Errorf("invalid file path, file path cannot be empty")
}
jsondata, err := json.MarshalIndent(a.groups, "", " ")
if err != nil {
return err
}
dir, filename := filepath.Split(a.filePath)
tmpfile, err := os.CreateTemp(dir, filename)
if err != nil {
return err
}
defer os.Remove(tmpfile.Name())
if _, err := tmpfile.Write(jsondata); err != nil {
return err
}
if err := tmpfile.Close(); err != nil {
return err
}
if err := os.Rename(tmpfile.Name(), a.filePath); err != nil {
return err
}
return nil
}
// Adapter (auto-save)
func (a *adapter) AddPolicy(sec, ptype string, rule []string) error {
a.lock.Lock()
defer a.lock.Unlock()
err := a.addPolicy(ptype, rule)
if err != nil {
return err
}
return a.savePolicyFile()
}
// BatchAdapter (auto-save)
func (a *adapter) AddPolicies(sec string, ptype string, rules [][]string) error {
a.lock.Lock()
defer a.lock.Unlock()
for _, rule := range rules {
err := a.addPolicy(ptype, rule)
if err != nil {
return err
}
}
return a.savePolicyFile()
}
func (a *adapter) addPolicy(ptype string, rule []string) error {
ok, err := a.hasPolicy(ptype, rule)
if err != nil {
return err
}
if ok {
// the policy is already there, nothing to add
return nil
}
username := ""
role := ""
domain := ""
resource := ""
actions := ""
if ptype == "p" {
username = rule[0]
domain = rule[1]
resource = rule[2]
actions = rule[3]
} else if ptype == "g" {
username = rule[0]
role = rule[1]
domain = rule[2]
} else {
return fmt.Errorf("unknown ptype: %s", ptype)
}
var group *Group = nil
for i := range a.groups {
if a.groups[i].Name == domain {
group = &a.groups[i]
}
}
if group == nil {
g := Group{
Name: domain,
}
a.groups = append(a.groups, g)
group = &g
}
if ptype == "p" {
if strings.HasPrefix(username, "role:") {
if group.Roles == nil {
group.Roles = make(map[string][]Role)
}
role := strings.TrimPrefix(username, "role:")
group.Roles[role] = append(group.Roles[role], Role{
Resource: resource,
Actions: actions,
})
} else {
group.Policies = append(group.Policies, Policy{
Username: rule[0],
Role: Role{
Resource: resource,
Actions: actions,
},
})
}
} else {
group.UserRoles = append(group.UserRoles, MapUserRole{
Username: username,
Role: strings.TrimPrefix(role, "role:"),
})
}
return nil
}
func (a *adapter) hasPolicy(ptype string, rule []string) (bool, error) {
var username string
var role string
var domain string
var resource string
var actions string
if ptype == "p" {
if len(rule) != 4 {
return false, fmt.Errorf("invalid rule length. must be 'user/role, domain, resource, actions'")
}
username = rule[0]
domain = rule[1]
resource = rule[2]
actions = rule[3]
} else if ptype == "g" {
username = rule[0]
role = rule[1]
domain = rule[2]
} else {
return false, fmt.Errorf("unknown ptype: %s", ptype)
}
var group *Group = nil
for _, g := range a.groups {
if g.Name == domain {
group = &g
break
}
}
if group == nil {
// if we can't find any group with that name, then the policy doesn't exist
return false, nil
}
if ptype == "p" {
isRole := false
if strings.HasPrefix(username, "role:") {
isRole = true
username = strings.TrimPrefix(username, "role:")
}
if isRole {
roles, ok := group.Roles[username]
if !ok {
// unknown role, policy doesn't exist
return false, nil
}
for _, role := range roles {
if role.Resource == resource && role.Actions == actions {
return true, nil
}
}
} else {
for _, p := range group.Policies {
if p.Username == username && p.Resource == resource && p.Actions == actions {
return true, nil
}
}
}
} else {
role = strings.TrimPrefix(role, "role:")
for _, user := range group.UserRoles {
if user.Username == username && user.Role == role {
return true, nil
}
}
}
return false, nil
}
// Adapter (auto-save)
func (a *adapter) RemovePolicy(sec string, ptype string, rule []string) error {
a.lock.Lock()
defer a.lock.Unlock()
err := a.removePolicy(ptype, rule)
if err != nil {
return err
}
return a.savePolicyFile()
}
// BatchAdapter (auto-save)
func (a *adapter) RemovePolicies(sec string, ptype string, rules [][]string) error {
a.lock.Lock()
defer a.lock.Unlock()
for _, rule := range rules {
err := a.removePolicy(ptype, rule)
if err != nil {
return err
}
}
return a.savePolicyFile()
}
func (a *adapter) removePolicy(ptype string, rule []string) error {
ok, err := a.hasPolicy(ptype, rule)
if err != nil {
return err
}
if !ok {
// the policy is not there, nothing to remove
return nil
}
username := ""
role := ""
domain := ""
resource := ""
actions := ""
if ptype == "p" {
username = rule[0]
domain = rule[1]
resource = rule[2]
actions = rule[3]
} else if ptype == "g" {
username = rule[0]
role = rule[1]
domain = rule[2]
} else {
return fmt.Errorf("unknown ptype: %s", ptype)
}
var group *Group = nil
for i := range a.groups {
if a.groups[i].Name == domain {
group = &a.groups[i]
}
}
if ptype == "p" {
isRole := false
if strings.HasPrefix(username, "role:") {
isRole = true
username = strings.TrimPrefix(username, "role:")
}
if isRole {
roles := group.Roles[username]
newRoles := []Role{}
for _, role := range roles {
if role.Resource == resource && role.Actions == actions {
continue
}
newRoles = append(newRoles, role)
}
group.Roles[username] = newRoles
} else {
policies := []Policy{}
for _, p := range group.Policies {
if p.Username == username && p.Resource == resource && p.Actions == actions {
continue
}
policies = append(policies, p)
}
group.Policies = policies
}
} else {
role = strings.TrimPrefix(role, "role:")
users := []MapUserRole{}
for _, user := range group.UserRoles {
if user.Username == username && user.Role == role {
continue
}
users = append(users, user)
}
group.UserRoles = users
}
return nil
}
// Adapter
func (a *adapter) RemoveFilteredPolicy(sec string, ptype string, fieldIndex int, fieldValues ...string) error {
return fmt.Errorf("not implemented")
}
func (a *adapter) GetAllGroupNames() []string {
a.lock.Lock()
defer a.lock.Unlock()
groups := []string{}
for _, group := range a.groups {
groups = append(groups, group.Name)
}
return groups
}
type Group struct {
Name string `json:"name"`
Roles map[string][]Role `json:"roles"`
UserRoles []MapUserRole `json:"userroles"`
Policies []Policy `json:"policies"`
}
type Role struct {
Resource string `json:"resource"`
Actions string `json:"actions"`
}
type MapUserRole struct {
Username string `json:"username"`
Role string `json:"role"`
}
type Policy struct {
Username string `json:"username"`
Role
}

Binary file not shown.

View File

@@ -1,215 +0,0 @@
package main
import (
"flag"
"fmt"
"os"
"strings"
"github.com/casbin/casbin/v2"
"github.com/casbin/casbin/v2/model"
"github.com/gobwas/glob"
)
func main() {
var subject string
var domain string
var object string
var action string
flag.StringVar(&subject, "subject", "$anon", "subject of this request")
flag.StringVar(&domain, "domain", "$none", "domain of this request")
flag.StringVar(&object, "object", "", "object of this request")
flag.StringVar(&action, "action", "", "action of this request")
flag.Parse()
m := model.NewModel()
m.AddDef("r", "r", "sub, dom, obj, act")
m.AddDef("p", "p", "sub, dom, obj, act")
m.AddDef("g", "g", "_, _, _")
m.AddDef("e", "e", "some(where (p.eft == allow))")
m.AddDef("m", "m", `g(r.sub, p.sub, r.dom) && r.dom == p.dom && ResourceMatch(r.obj, r.dom, p.obj) && ActionMatch(r.act, p.act) || r.sub == "$superuser"`)
a := NewAdapter("./policy.json")
e, err := casbin.NewEnforcer(m, a)
if err != nil {
fmt.Printf("error: %s\n", err)
os.Exit(1)
}
e.AddFunction("ResourceMatch", ResourceMatchFunc)
e.AddFunction("ActionMatch", ActionMatchFunc)
/*
if err := addGroup(e, "foobar"); err != nil {
fmt.Printf("error: %s\n", err)
os.Exit(1)
}
if err := addGroupUser(e, "foobar", "franz", "admin"); err != nil {
fmt.Printf("error: %s\n", err)
os.Exit(1)
}
if err := addGroupUser(e, "foobar", "$anon", "anonymous"); err != nil {
fmt.Printf("error: %s\n", err)
os.Exit(1)
}
e.RemovePolicy("bob", "igelcamp", "processid:*", "COMMAND")
e.AddPolicy("bob", "igelcamp", "processid:bob-*", "COMMAND")
*/
ok, reason, err := e.EnforceEx(subject, domain, object, action)
if err != nil {
fmt.Printf("error: %s\n", err)
os.Exit(1)
}
if ok {
fmt.Printf("OK: %v\n", reason)
} else {
fmt.Printf("not OK\n")
}
}
func ResourceMatch(request, domain, policy string) bool {
reqPrefix, reqResource := getPrefix(request)
polPrefix, polResource := getPrefix(policy)
if reqPrefix != polPrefix {
return false
}
fmt.Printf("prefix: %s\n", reqPrefix)
fmt.Printf("requested resource: %s\n", reqResource)
fmt.Printf("requested domain: %s\n", domain)
fmt.Printf("policy resource: %s\n", polResource)
var match bool
var err error
if reqPrefix == "processid" {
match, err = Match(polResource, reqResource)
if err != nil {
return false
}
} else if reqPrefix == "api" {
match, err = Match(polResource, reqResource, rune('/'))
if err != nil {
return false
}
} else if reqPrefix == "fs" {
match, err = Match(polResource, reqResource, rune('/'))
if err != nil {
return false
}
} else if reqPrefix == "rtmp" {
match, err = Match(polResource, reqResource)
if err != nil {
return false
}
} else if reqPrefix == "srt" {
match, err = Match(polResource, reqResource)
if err != nil {
return false
}
} else {
match, err = Match(polResource, reqResource)
if err != nil {
return false
}
}
fmt.Printf("match: %v\n", match)
return match
}
func ResourceMatchFunc(args ...interface{}) (interface{}, error) {
name1 := args[0].(string)
name2 := args[1].(string)
name3 := args[2].(string)
return (bool)(ResourceMatch(name1, name2, name3)), nil
}
func ActionMatch(request string, policy string) bool {
request = strings.ToUpper(request)
actions := strings.Split(strings.ToUpper(policy), "|")
if len(actions) == 0 {
return false
}
for _, a := range actions {
if request == a {
return true
}
}
return false
}
func ActionMatchFunc(args ...interface{}) (interface{}, error) {
name1 := args[0].(string)
name2 := args[1].(string)
return (bool)(ActionMatch(name1, name2)), nil
}
func getPrefix(s string) (string, string) {
splits := strings.SplitN(s, ":", 2)
if len(splits) == 0 {
return "", ""
}
if len(splits) == 1 {
return "", splits[0]
}
return splits[0], splits[1]
}
func Match(pattern, name string, separators ...rune) (bool, error) {
g, err := glob.Compile(pattern, separators...)
if err != nil {
return false, err
}
return g.Match(name), nil
}
func addGroup(e *casbin.Enforcer, name string) error {
rules := [][]string{}
rules = append(rules, []string{"role:admin", name, "api:/process/**", "GET|POST|PUT|DELETE"})
rules = append(rules, []string{"role:admin", name, "processid:*", "CONFIG|PROGRESS|REPORT|METADATA|COMMAND"})
rules = append(rules, []string{"role:admin", name, "rtmp:" + name + "/*", "PUBLISH|PLAY"})
rules = append(rules, []string{"role:admin", name, "srt:" + name + "/*", "PUBLISH|PLAY"})
rules = append(rules, []string{"role:admin", name, "fs:/" + name + "/**", "GET|POST|PUT|DELETE"})
rules = append(rules, []string{"role:admin", name, "fs:/memfs/" + name + "/**", "GET|POST|PUT|DELETE"})
rules = append(rules, []string{"role:user", name, "api:/process/**", "GET"})
rules = append(rules, []string{"role:user", name, "processid:*", "PROGRESS"})
rules = append(rules, []string{"role:user", name, "rtmp:" + name + "/*", "PLAY"})
rules = append(rules, []string{"role:user", name, "srt:" + name + "/*", "PLAY"})
rules = append(rules, []string{"role:user", name, "fs:/" + name + "/**", "GET"})
rules = append(rules, []string{"role:user", name, "fs:/memfs/" + name + "/**", "GET"})
rules = append(rules, []string{"role:anonymous", name, "rtmp:" + name + "/*", "PLAY"})
rules = append(rules, []string{"role:anonymous", name, "srt:" + name + "/*", "PLAY"})
rules = append(rules, []string{"role:anonymous", name, "fs:/" + name + "/**", "GET"})
rules = append(rules, []string{"role:anonymous", name, "fs:/memfs/" + name + "/**", "GET"})
_, err := e.AddPolicies(rules)
return err
}
func addGroupUser(e *casbin.Enforcer, group, username, role string) error {
_, err := e.AddGroupingPolicy(username, "role:"+role, group)
return err
}

View File

@@ -1,17 +0,0 @@
p, admin, igelcamp, api:/process/**, GET|POST|PUT|DELETE
p, admin, igelcamp, processid:*, GET|POST|PUT|DELETE
p, admin, igelcamp, rtmp:*, PUBLISH|PLAY
p, admin, igelcamp, srt:*, PUBLISH|PLAY
p, admin, igelcamp, fs:/igelcamp/**, GET|POST|PUT|DELETE
p, admin, igelcamp, fs:/memfs/igelcamp/**, GET|POST|PUT|DELETE
p, user, igelcamp, api:/process/**, GET
p, user, igelcamp, processid:*, GET
p, user, igelcamp, rtmp:*, PLAY
p, user, igelcamp, srt:*, PLAY
p, user, igelcamp, fs:/igelcamp/**, GET
p, user, igelcamp, fs:/memfs/igelcamp/**, GET
p, anonymous, $none, fs:/*, GET
g, alice, admin, igelcamp
g, alice, anonymous, $none
g, bob, user, igelcamp
Can't render this file because it has a wrong number of fields in line 15.

View File

@@ -1,206 +0,0 @@
[
{
"name": "igelcamp",
"roles": {
"admin": [
{
"resource": "api:/process/**",
"actions": "GET|POST|PUT|DELETE"
},
{
"resource": "processid:*",
"actions": "CONFIG|PROGRESS|REPORT|METADATA|COMMAND"
},
{
"resource": "rtmp:igelcamp/*",
"actions": "PUBLISH|PLAY"
},
{
"resource": "srt:igelcamp/*",
"actions": "PUBLISH|PLAY"
},
{
"resource": "fs:/igelcamp/**",
"actions": "GET|POST|PUT|DELETE"
},
{
"resource": "fs:/memfs/igelcamp/**",
"actions": "GET|POST|PUT|DELETE"
}
],
"anonymous": [
{
"resource": "rtmp:igelcamp/*",
"actions": "PLAY"
},
{
"resource": "srt:igelcamp/*",
"actions": "PLAY"
},
{
"resource": "fs:/igelcamp/**",
"actions": "GET"
},
{
"resource": "fs:/memfs/igelcamp/**",
"actions": "GET"
}
],
"user": [
{
"resource": "api:/process/**",
"actions": "GET"
},
{
"resource": "processid:*",
"actions": "PROGRESS"
},
{
"resource": "rtmp:igelcamp/*",
"actions": "PLAY"
},
{
"resource": "srt:igelcamp/*",
"actions": "PLAY"
},
{
"resource": "fs:/igelcamp/**",
"actions": "GET"
},
{
"resource": "fs:/memfs/igelcamp/**",
"actions": "GET"
}
]
},
"userroles": [
{
"username": "alice",
"role": "admin"
},
{
"username": "bob",
"role": "user"
},
{
"username": "$anon",
"role": "anonymous"
}
],
"policies": [
{
"username": "bob",
"resource": "processid:bob-*",
"actions": "COMMAND"
}
]
},
{
"name": "$none",
"roles": {
"anonymous": [
{
"resource": "fs:/*",
"actions": "GET"
}
]
},
"userroles": [
{
"username": "$anon",
"role": "anonymous"
},
{
"username": "alice",
"role": "anonymous"
},
{
"username": "bob",
"role": "anonymous"
}
],
"policies": null
},
{
"name": "foobar",
"roles": {
"admin": [
{
"resource": "processid:*",
"actions": "CONFIG|PROGRESS|REPORT|METADATA|COMMAND"
},
{
"resource": "rtmp:foobar/*",
"actions": "PUBLISH|PLAY"
},
{
"resource": "srt:foobar/*",
"actions": "PUBLISH|PLAY"
},
{
"resource": "fs:/foobar/**",
"actions": "GET|POST|PUT|DELETE"
},
{
"resource": "fs:/memfs/foobar/**",
"actions": "GET|POST|PUT|DELETE"
}
],
"anonymous": [
{
"resource": "rtmp:foobar/*",
"actions": "PLAY"
},
{
"resource": "srt:foobar/*",
"actions": "PLAY"
},
{
"resource": "fs:/foobar/**",
"actions": "GET"
},
{
"resource": "fs:/memfs/foobar/**",
"actions": "GET"
}
],
"user": [
{
"resource": "api:/process/**",
"actions": "GET"
},
{
"resource": "processid:*",
"actions": "PROGRESS"
},
{
"resource": "rtmp:foobar/*",
"actions": "PLAY"
},
{
"resource": "srt:foobar/*",
"actions": "PLAY"
},
{
"resource": "fs:/foobar/**",
"actions": "GET"
},
{
"resource": "fs:/memfs/foobar/**",
"actions": "GET"
}
]
},
"userroles": [
{
"username": "franz",
"role": "admin"
},
{
"username": "$anon",
"role": "anonymous"
}
],
"policies": null
}
]

View File

@@ -1,93 +0,0 @@
[
{
"name": "alice",
"superuser": false,
"auth": {
"api": {
"userpass": {
"enable": true,
"username": "foo",
"password": "bar"
},
"auth0": {
"enable": true,
"user": "google|42",
"tenant": "tenant1"
}
},
"http": {
"basic": {
"enable": true,
"username": "bar",
"password": "baz"
}
},
"rtmp": {
"enable": true,
"token": "abc123"
},
"srt": {
"enable": true,
"token": "xyz987"
}
}
},
{
"name": "bob",
"superuser": true,
"auth": {
"api": {
"userpass": {
"enable": true,
"username": "foo",
"password": "baz"
},
"auth0": {
"enable": true,
"user": "github|88",
"tenant": "tenant2"
}
},
"http": {
"basic": {
"enable": true,
"username": "boz",
"password": "bok"
}
},
"rtmp": {
"enable": true,
"token": "abc456"
},
"srt": {
"enable": true,
"token": "xyz654"
}
}
},
{
"name": "$anon",
"superuser": false,
"auth": {
"api": {
"userpass": {
"enable": false
},
"auth0": {
"enable": false
}
},
"http": {
"basic": {
"enable": false
}
},
"rtmp": {
"enable": false
},
"srt": {
"enable": false
}
}
}
]

View File

@@ -17,6 +17,7 @@ import (
"github.com/datarhei/core/v16/encoding/json" "github.com/datarhei/core/v16/encoding/json"
"github.com/datarhei/core/v16/ffmpeg" "github.com/datarhei/core/v16/ffmpeg"
"github.com/datarhei/core/v16/ffmpeg/skills" "github.com/datarhei/core/v16/ffmpeg/skills"
"github.com/datarhei/core/v16/iam"
"github.com/datarhei/core/v16/io/fs" "github.com/datarhei/core/v16/io/fs"
"github.com/datarhei/core/v16/restream" "github.com/datarhei/core/v16/restream"
"github.com/datarhei/core/v16/restream/app" "github.com/datarhei/core/v16/restream/app"
@@ -502,6 +503,7 @@ func importV1(fs fs.Filesystem, path string, cfg importConfig) (store.StoreData,
} }
r := store.NewStoreData() r := store.NewStoreData()
r.Version = 4
jsondata, err := fs.ReadFile(path) jsondata, err := fs.ReadFile(path)
if err != nil { if err != nil {
@@ -1428,17 +1430,30 @@ func probeInput(binary string, config app.Config) app.Probe {
return app.Probe{} return app.Probe{}
} }
iam, _ := iam.NewIAM(iam.Config{
FS: dummyfs,
Superuser: iam.User{
Name: "foobar",
},
JWTRealm: "",
JWTSecret: "",
Logger: nil,
})
iam.AddPolicy("$anon", "$none", "process:*", "CREATE|GET|DELETE|PROBE")
rs, err := restream.New(restream.Config{ rs, err := restream.New(restream.Config{
FFmpeg: ffmpeg, FFmpeg: ffmpeg,
Store: store, Store: store,
IAM: iam,
}) })
if err != nil { if err != nil {
return app.Probe{} return app.Probe{}
} }
rs.AddProcess(&config) rs.AddProcess(&config)
probe := rs.Probe(config.ID) probe := rs.Probe(config.ID, "", "")
rs.DeleteProcess(config.ID) rs.DeleteProcess(config.ID, "", "")
return probe return probe
} }

View File

@@ -44,6 +44,7 @@ type ProcessConfigLimits struct {
// ProcessConfig represents the configuration of an ffmpeg process // ProcessConfig represents the configuration of an ffmpeg process
type ProcessConfig struct { type ProcessConfig struct {
ID string `json:"id"` ID string `json:"id"`
Group string `json:"group"`
Type string `json:"type" validate:"oneof='ffmpeg' ''" jsonschema:"enum=ffmpeg,enum="` Type string `json:"type" validate:"oneof='ffmpeg' ''" jsonschema:"enum=ffmpeg,enum="`
Reference string `json:"reference"` Reference string `json:"reference"`
Input []ProcessConfigIO `json:"input" validate:"required"` Input []ProcessConfigIO `json:"input" validate:"required"`
@@ -60,6 +61,7 @@ type ProcessConfig struct {
func (cfg *ProcessConfig) Marshal() *app.Config { func (cfg *ProcessConfig) Marshal() *app.Config {
p := &app.Config{ p := &app.Config{
ID: cfg.ID, ID: cfg.ID,
Group: cfg.Group,
Reference: cfg.Reference, Reference: cfg.Reference,
Options: cfg.Options, Options: cfg.Options,
Reconnect: cfg.Reconnect, Reconnect: cfg.Reconnect,
@@ -139,6 +141,7 @@ func (cfg *ProcessConfig) Unmarshal(c *app.Config) {
} }
cfg.ID = c.ID cfg.ID = c.ID
cfg.Group = c.Group
cfg.Reference = c.Reference cfg.Reference = c.Reference
cfg.Type = "ffmpeg" cfg.Type = "ffmpeg"
cfg.Reconnect = c.Reconnect cfg.Reconnect = c.Reconnect

View File

@@ -127,8 +127,10 @@ type ComplexityRoot struct {
Process struct { Process struct {
Config func(childComplexity int) int Config func(childComplexity int) int
CreatedAt func(childComplexity int) int CreatedAt func(childComplexity int) int
Group func(childComplexity int) int
ID func(childComplexity int) int ID func(childComplexity int) int
Metadata func(childComplexity int) int Metadata func(childComplexity int) int
Owner func(childComplexity int) int
Reference func(childComplexity int) int Reference func(childComplexity int) int
Report func(childComplexity int) int Report func(childComplexity int) int
State func(childComplexity int) int State func(childComplexity int) int
@@ -137,11 +139,13 @@ type ComplexityRoot struct {
ProcessConfig struct { ProcessConfig struct {
Autostart func(childComplexity int) int Autostart func(childComplexity int) int
Group func(childComplexity int) int
ID func(childComplexity int) int ID func(childComplexity int) int
Input func(childComplexity int) int Input func(childComplexity int) int
Limits func(childComplexity int) int Limits func(childComplexity int) int
Options func(childComplexity int) int Options func(childComplexity int) int
Output func(childComplexity int) int Output func(childComplexity int) int
Owner func(childComplexity int) int
Reconnect func(childComplexity int) int Reconnect func(childComplexity int) int
ReconnectDelaySeconds func(childComplexity int) int ReconnectDelaySeconds func(childComplexity int) int
Reference func(childComplexity int) int Reference func(childComplexity int) int
@@ -236,10 +240,10 @@ type ComplexityRoot struct {
Log func(childComplexity int) int Log func(childComplexity int) int
Metrics func(childComplexity int, query models.MetricsInput) int Metrics func(childComplexity int, query models.MetricsInput) int
Ping func(childComplexity int) int Ping func(childComplexity int) int
PlayoutStatus func(childComplexity int, id string, input string) int PlayoutStatus func(childComplexity int, id string, group *string, input string) int
Probe func(childComplexity int, id string) int Probe func(childComplexity int, id string, group *string) int
Process func(childComplexity int, id string) int Process func(childComplexity int, id string, group *string) int
Processes func(childComplexity int) int Processes func(childComplexity int, idpattern *string, refpattern *string, group *string) int
} }
RawAVstream struct { RawAVstream struct {
@@ -283,10 +287,10 @@ type QueryResolver interface {
About(ctx context.Context) (*models.About, error) About(ctx context.Context) (*models.About, error)
Log(ctx context.Context) ([]string, error) Log(ctx context.Context) ([]string, error)
Metrics(ctx context.Context, query models.MetricsInput) (*models.Metrics, error) Metrics(ctx context.Context, query models.MetricsInput) (*models.Metrics, error)
PlayoutStatus(ctx context.Context, id string, input string) (*models.RawAVstream, error) PlayoutStatus(ctx context.Context, id string, group *string, input string) (*models.RawAVstream, error)
Processes(ctx context.Context) ([]*models.Process, error) Processes(ctx context.Context, idpattern *string, refpattern *string, group *string) ([]*models.Process, error)
Process(ctx context.Context, id string) (*models.Process, error) Process(ctx context.Context, id string, group *string) (*models.Process, error)
Probe(ctx context.Context, id string) (*models.Probe, error) Probe(ctx context.Context, id string, group *string) (*models.Probe, error)
} }
type executableSchema struct { type executableSchema struct {
@@ -675,6 +679,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Process.CreatedAt(childComplexity), true return e.complexity.Process.CreatedAt(childComplexity), true
case "Process.group":
if e.complexity.Process.Group == nil {
break
}
return e.complexity.Process.Group(childComplexity), true
case "Process.id": case "Process.id":
if e.complexity.Process.ID == nil { if e.complexity.Process.ID == nil {
break break
@@ -689,6 +700,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Process.Metadata(childComplexity), true return e.complexity.Process.Metadata(childComplexity), true
case "Process.owner":
if e.complexity.Process.Owner == nil {
break
}
return e.complexity.Process.Owner(childComplexity), true
case "Process.reference": case "Process.reference":
if e.complexity.Process.Reference == nil { if e.complexity.Process.Reference == nil {
break break
@@ -724,6 +742,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.ProcessConfig.Autostart(childComplexity), true return e.complexity.ProcessConfig.Autostart(childComplexity), true
case "ProcessConfig.group":
if e.complexity.ProcessConfig.Group == nil {
break
}
return e.complexity.ProcessConfig.Group(childComplexity), true
case "ProcessConfig.id": case "ProcessConfig.id":
if e.complexity.ProcessConfig.ID == nil { if e.complexity.ProcessConfig.ID == nil {
break break
@@ -759,6 +784,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.ProcessConfig.Output(childComplexity), true return e.complexity.ProcessConfig.Output(childComplexity), true
case "ProcessConfig.owner":
if e.complexity.ProcessConfig.Owner == nil {
break
}
return e.complexity.ProcessConfig.Owner(childComplexity), true
case "ProcessConfig.reconnect": case "ProcessConfig.reconnect":
if e.complexity.ProcessConfig.Reconnect == nil { if e.complexity.ProcessConfig.Reconnect == nil {
break break
@@ -1243,7 +1275,7 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return 0, false return 0, false
} }
return e.complexity.Query.PlayoutStatus(childComplexity, args["id"].(string), args["input"].(string)), true return e.complexity.Query.PlayoutStatus(childComplexity, args["id"].(string), args["group"].(*string), args["input"].(string)), true
case "Query.probe": case "Query.probe":
if e.complexity.Query.Probe == nil { if e.complexity.Query.Probe == nil {
@@ -1255,7 +1287,7 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return 0, false return 0, false
} }
return e.complexity.Query.Probe(childComplexity, args["id"].(string)), true return e.complexity.Query.Probe(childComplexity, args["id"].(string), args["group"].(*string)), true
case "Query.process": case "Query.process":
if e.complexity.Query.Process == nil { if e.complexity.Query.Process == nil {
@@ -1267,14 +1299,19 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return 0, false return 0, false
} }
return e.complexity.Query.Process(childComplexity, args["id"].(string)), true return e.complexity.Query.Process(childComplexity, args["id"].(string), args["group"].(*string)), true
case "Query.processes": case "Query.processes":
if e.complexity.Query.Processes == nil { if e.complexity.Query.Processes == nil {
break break
} }
return e.complexity.Query.Processes(childComplexity), true args, err := ec.field_Query_processes_args(context.TODO(), rawArgs)
if err != nil {
return 0, false
}
return e.complexity.Query.Processes(childComplexity, args["idpattern"].(*string), args["refpattern"].(*string), args["group"].(*string)), true
case "RawAVstream.aqueue": case "RawAVstream.aqueue":
if e.complexity.RawAVstream.Aqueue == nil { if e.complexity.RawAVstream.Aqueue == nil {
@@ -1561,7 +1598,7 @@ type Metric {
} }
`, BuiltIn: false}, `, BuiltIn: false},
{Name: "../playout.graphqls", Input: `extend type Query { {Name: "../playout.graphqls", Input: `extend type Query {
playoutStatus(id: ID!, input: ID!): RawAVstream playoutStatus(id: ID!, group: String, input: ID!): RawAVstream
} }
type RawAVstreamIO { type RawAVstreamIO {
@@ -1597,9 +1634,9 @@ type RawAVstream {
} }
`, BuiltIn: false}, `, BuiltIn: false},
{Name: "../process.graphqls", Input: `extend type Query { {Name: "../process.graphqls", Input: `extend type Query {
processes: [Process!]! processes(idpattern: String, refpattern: String, group: String): [Process!]!
process(id: ID!): Process process(id: ID!, group: String): Process
probe(id: ID!): Probe! probe(id: ID!, group: String): Probe!
} }
type ProcessConfigIO { type ProcessConfigIO {
@@ -1616,6 +1653,8 @@ type ProcessConfigLimits {
type ProcessConfig { type ProcessConfig {
id: String! id: String!
owner: String!
group: String!
type: String! type: String!
reference: String! reference: String!
input: [ProcessConfigIO!]! input: [ProcessConfigIO!]!
@@ -1666,6 +1705,8 @@ type ProcessReport implements IProcessReportHistoryEntry {
type Process { type Process {
id: String! id: String!
owner: String!
group: String!
type: String! type: String!
reference: String! reference: String!
created_at: Time! created_at: Time!
@@ -1840,15 +1881,24 @@ func (ec *executionContext) field_Query_playoutStatus_args(ctx context.Context,
} }
} }
args["id"] = arg0 args["id"] = arg0
var arg1 string var arg1 *string
if tmp, ok := rawArgs["input"]; ok { if tmp, ok := rawArgs["group"]; ok {
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("input")) ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("group"))
arg1, err = ec.unmarshalNID2string(ctx, tmp) arg1, err = ec.unmarshalOString2ᚖstring(ctx, tmp)
if err != nil { if err != nil {
return nil, err return nil, err
} }
} }
args["input"] = arg1 args["group"] = arg1
var arg2 string
if tmp, ok := rawArgs["input"]; ok {
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("input"))
arg2, err = ec.unmarshalNID2string(ctx, tmp)
if err != nil {
return nil, err
}
}
args["input"] = arg2
return args, nil return args, nil
} }
@@ -1864,6 +1914,15 @@ func (ec *executionContext) field_Query_probe_args(ctx context.Context, rawArgs
} }
} }
args["id"] = arg0 args["id"] = arg0
var arg1 *string
if tmp, ok := rawArgs["group"]; ok {
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("group"))
arg1, err = ec.unmarshalOString2ᚖstring(ctx, tmp)
if err != nil {
return nil, err
}
}
args["group"] = arg1
return args, nil return args, nil
} }
@@ -1879,6 +1938,48 @@ func (ec *executionContext) field_Query_process_args(ctx context.Context, rawArg
} }
} }
args["id"] = arg0 args["id"] = arg0
var arg1 *string
if tmp, ok := rawArgs["group"]; ok {
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("group"))
arg1, err = ec.unmarshalOString2ᚖstring(ctx, tmp)
if err != nil {
return nil, err
}
}
args["group"] = arg1
return args, nil
}
func (ec *executionContext) field_Query_processes_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
var err error
args := map[string]interface{}{}
var arg0 *string
if tmp, ok := rawArgs["idpattern"]; ok {
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("idpattern"))
arg0, err = ec.unmarshalOString2ᚖstring(ctx, tmp)
if err != nil {
return nil, err
}
}
args["idpattern"] = arg0
var arg1 *string
if tmp, ok := rawArgs["refpattern"]; ok {
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("refpattern"))
arg1, err = ec.unmarshalOString2ᚖstring(ctx, tmp)
if err != nil {
return nil, err
}
}
args["refpattern"] = arg1
var arg2 *string
if tmp, ok := rawArgs["group"]; ok {
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("group"))
arg2, err = ec.unmarshalOString2ᚖstring(ctx, tmp)
if err != nil {
return nil, err
}
}
args["group"] = arg2
return args, nil return args, nil
} }
@@ -4275,6 +4376,94 @@ func (ec *executionContext) fieldContext_Process_id(ctx context.Context, field g
return fc, nil return fc, nil
} }
func (ec *executionContext) _Process_owner(ctx context.Context, field graphql.CollectedField, obj *models.Process) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Process_owner(ctx, field)
if err != nil {
return graphql.Null
}
ctx = graphql.WithFieldContext(ctx, fc)
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.Owner, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(string)
fc.Result = res
return ec.marshalNString2string(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_Process_owner(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "Process",
Field: field,
IsMethod: false,
IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
return nil, errors.New("field of type String does not have child fields")
},
}
return fc, nil
}
func (ec *executionContext) _Process_group(ctx context.Context, field graphql.CollectedField, obj *models.Process) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Process_group(ctx, field)
if err != nil {
return graphql.Null
}
ctx = graphql.WithFieldContext(ctx, fc)
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.Group, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(string)
fc.Result = res
return ec.marshalNString2string(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_Process_group(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "Process",
Field: field,
IsMethod: false,
IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
return nil, errors.New("field of type String does not have child fields")
},
}
return fc, nil
}
func (ec *executionContext) _Process_type(ctx context.Context, field graphql.CollectedField, obj *models.Process) (ret graphql.Marshaler) { func (ec *executionContext) _Process_type(ctx context.Context, field graphql.CollectedField, obj *models.Process) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Process_type(ctx, field) fc, err := ec.fieldContext_Process_type(ctx, field)
if err != nil { if err != nil {
@@ -4448,6 +4637,10 @@ func (ec *executionContext) fieldContext_Process_config(ctx context.Context, fie
switch field.Name { switch field.Name {
case "id": case "id":
return ec.fieldContext_ProcessConfig_id(ctx, field) return ec.fieldContext_ProcessConfig_id(ctx, field)
case "owner":
return ec.fieldContext_ProcessConfig_owner(ctx, field)
case "group":
return ec.fieldContext_ProcessConfig_group(ctx, field)
case "type": case "type":
return ec.fieldContext_ProcessConfig_type(ctx, field) return ec.fieldContext_ProcessConfig_type(ctx, field)
case "reference": case "reference":
@@ -4678,6 +4871,94 @@ func (ec *executionContext) fieldContext_ProcessConfig_id(ctx context.Context, f
return fc, nil return fc, nil
} }
func (ec *executionContext) _ProcessConfig_owner(ctx context.Context, field graphql.CollectedField, obj *models.ProcessConfig) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_ProcessConfig_owner(ctx, field)
if err != nil {
return graphql.Null
}
ctx = graphql.WithFieldContext(ctx, fc)
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.Owner, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(string)
fc.Result = res
return ec.marshalNString2string(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_ProcessConfig_owner(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "ProcessConfig",
Field: field,
IsMethod: false,
IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
return nil, errors.New("field of type String does not have child fields")
},
}
return fc, nil
}
func (ec *executionContext) _ProcessConfig_group(ctx context.Context, field graphql.CollectedField, obj *models.ProcessConfig) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_ProcessConfig_group(ctx, field)
if err != nil {
return graphql.Null
}
ctx = graphql.WithFieldContext(ctx, fc)
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.Group, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(string)
fc.Result = res
return ec.marshalNString2string(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_ProcessConfig_group(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "ProcessConfig",
Field: field,
IsMethod: false,
IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
return nil, errors.New("field of type String does not have child fields")
},
}
return fc, nil
}
func (ec *executionContext) _ProcessConfig_type(ctx context.Context, field graphql.CollectedField, obj *models.ProcessConfig) (ret graphql.Marshaler) { func (ec *executionContext) _ProcessConfig_type(ctx context.Context, field graphql.CollectedField, obj *models.ProcessConfig) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_ProcessConfig_type(ctx, field) fc, err := ec.fieldContext_ProcessConfig_type(ctx, field)
if err != nil { if err != nil {
@@ -8071,7 +8352,7 @@ func (ec *executionContext) _Query_playoutStatus(ctx context.Context, field grap
}() }()
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children ctx = rctx // use context from middleware stack in children
return ec.resolvers.Query().PlayoutStatus(rctx, fc.Args["id"].(string), fc.Args["input"].(string)) return ec.resolvers.Query().PlayoutStatus(rctx, fc.Args["id"].(string), fc.Args["group"].(*string), fc.Args["input"].(string))
}) })
if err != nil { if err != nil {
ec.Error(ctx, err) ec.Error(ctx, err)
@@ -8155,7 +8436,7 @@ func (ec *executionContext) _Query_processes(ctx context.Context, field graphql.
}() }()
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children ctx = rctx // use context from middleware stack in children
return ec.resolvers.Query().Processes(rctx) return ec.resolvers.Query().Processes(rctx, fc.Args["idpattern"].(*string), fc.Args["refpattern"].(*string), fc.Args["group"].(*string))
}) })
if err != nil { if err != nil {
ec.Error(ctx, err) ec.Error(ctx, err)
@@ -8182,6 +8463,10 @@ func (ec *executionContext) fieldContext_Query_processes(ctx context.Context, fi
switch field.Name { switch field.Name {
case "id": case "id":
return ec.fieldContext_Process_id(ctx, field) return ec.fieldContext_Process_id(ctx, field)
case "owner":
return ec.fieldContext_Process_owner(ctx, field)
case "group":
return ec.fieldContext_Process_group(ctx, field)
case "type": case "type":
return ec.fieldContext_Process_type(ctx, field) return ec.fieldContext_Process_type(ctx, field)
case "reference": case "reference":
@@ -8200,6 +8485,17 @@ func (ec *executionContext) fieldContext_Query_processes(ctx context.Context, fi
return nil, fmt.Errorf("no field named %q was found under type Process", field.Name) return nil, fmt.Errorf("no field named %q was found under type Process", field.Name)
}, },
} }
defer func() {
if r := recover(); r != nil {
err = ec.Recover(ctx, r)
ec.Error(ctx, err)
}
}()
ctx = graphql.WithFieldContext(ctx, fc)
if fc.Args, err = ec.field_Query_processes_args(ctx, field.ArgumentMap(ec.Variables)); err != nil {
ec.Error(ctx, err)
return
}
return fc, nil return fc, nil
} }
@@ -8217,7 +8513,7 @@ func (ec *executionContext) _Query_process(ctx context.Context, field graphql.Co
}() }()
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children ctx = rctx // use context from middleware stack in children
return ec.resolvers.Query().Process(rctx, fc.Args["id"].(string)) return ec.resolvers.Query().Process(rctx, fc.Args["id"].(string), fc.Args["group"].(*string))
}) })
if err != nil { if err != nil {
ec.Error(ctx, err) ec.Error(ctx, err)
@@ -8241,6 +8537,10 @@ func (ec *executionContext) fieldContext_Query_process(ctx context.Context, fiel
switch field.Name { switch field.Name {
case "id": case "id":
return ec.fieldContext_Process_id(ctx, field) return ec.fieldContext_Process_id(ctx, field)
case "owner":
return ec.fieldContext_Process_owner(ctx, field)
case "group":
return ec.fieldContext_Process_group(ctx, field)
case "type": case "type":
return ec.fieldContext_Process_type(ctx, field) return ec.fieldContext_Process_type(ctx, field)
case "reference": case "reference":
@@ -8287,7 +8587,7 @@ func (ec *executionContext) _Query_probe(ctx context.Context, field graphql.Coll
}() }()
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children ctx = rctx // use context from middleware stack in children
return ec.resolvers.Query().Probe(rctx, fc.Args["id"].(string)) return ec.resolvers.Query().Probe(rctx, fc.Args["id"].(string), fc.Args["group"].(*string))
}) })
if err != nil { if err != nil {
ec.Error(ctx, err) ec.Error(ctx, err)
@@ -11282,7 +11582,12 @@ func (ec *executionContext) unmarshalInputMetricInput(ctx context.Context, obj i
asMap[k] = v asMap[k] = v
} }
for k, v := range asMap { fieldsInOrder := [...]string{"name", "labels"}
for _, k := range fieldsInOrder {
v, ok := asMap[k]
if !ok {
continue
}
switch k { switch k {
case "name": case "name":
var err error var err error
@@ -11313,7 +11618,12 @@ func (ec *executionContext) unmarshalInputMetricsInput(ctx context.Context, obj
asMap[k] = v asMap[k] = v
} }
for k, v := range asMap { fieldsInOrder := [...]string{"timerange_seconds", "interval_seconds", "metrics"}
for _, k := range fieldsInOrder {
v, ok := asMap[k]
if !ok {
continue
}
switch k { switch k {
case "timerange_seconds": case "timerange_seconds":
var err error var err error
@@ -11938,6 +12248,20 @@ func (ec *executionContext) _Process(ctx context.Context, sel ast.SelectionSet,
out.Values[i] = ec._Process_id(ctx, field, obj) out.Values[i] = ec._Process_id(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
}
case "owner":
out.Values[i] = ec._Process_owner(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
}
case "group":
out.Values[i] = ec._Process_group(ctx, field, obj)
if out.Values[i] == graphql.Null { if out.Values[i] == graphql.Null {
invalids++ invalids++
} }
@@ -12012,6 +12336,20 @@ func (ec *executionContext) _ProcessConfig(ctx context.Context, sel ast.Selectio
out.Values[i] = ec._ProcessConfig_id(ctx, field, obj) out.Values[i] = ec._ProcessConfig_id(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
}
case "owner":
out.Values[i] = ec._ProcessConfig_owner(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
}
case "group":
out.Values[i] = ec._ProcessConfig_group(ctx, field, obj)
if out.Values[i] == graphql.Null { if out.Values[i] == graphql.Null {
invalids++ invalids++
} }

View File

@@ -13,6 +13,9 @@ import (
type IProcessReportHistoryEntry interface { type IProcessReportHistoryEntry interface {
IsIProcessReportHistoryEntry() IsIProcessReportHistoryEntry()
GetCreatedAt() time.Time
GetPrelude() []string
GetLog() []*ProcessReportLogEntry
} }
type AVStream struct { type AVStream struct {
@@ -102,6 +105,8 @@ type ProbeIo struct {
type Process struct { type Process struct {
ID string `json:"id"` ID string `json:"id"`
Owner string `json:"owner"`
Group string `json:"group"`
Type string `json:"type"` Type string `json:"type"`
Reference string `json:"reference"` Reference string `json:"reference"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
@@ -113,6 +118,8 @@ type Process struct {
type ProcessConfig struct { type ProcessConfig struct {
ID string `json:"id"` ID string `json:"id"`
Owner string `json:"owner"`
Group string `json:"group"`
Type string `json:"type"` Type string `json:"type"`
Reference string `json:"reference"` Reference string `json:"reference"`
Input []*ProcessConfigIo `json:"input"` Input []*ProcessConfigIo `json:"input"`
@@ -145,6 +152,27 @@ type ProcessReport struct {
} }
func (ProcessReport) IsIProcessReportHistoryEntry() {} func (ProcessReport) IsIProcessReportHistoryEntry() {}
func (this ProcessReport) GetCreatedAt() time.Time { return this.CreatedAt }
func (this ProcessReport) GetPrelude() []string {
if this.Prelude == nil {
return nil
}
interfaceSlice := make([]string, 0, len(this.Prelude))
for _, concrete := range this.Prelude {
interfaceSlice = append(interfaceSlice, concrete)
}
return interfaceSlice
}
func (this ProcessReport) GetLog() []*ProcessReportLogEntry {
if this.Log == nil {
return nil
}
interfaceSlice := make([]*ProcessReportLogEntry, 0, len(this.Log))
for _, concrete := range this.Log {
interfaceSlice = append(interfaceSlice, concrete)
}
return interfaceSlice
}
type ProcessReportHistoryEntry struct { type ProcessReportHistoryEntry struct {
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
@@ -153,6 +181,27 @@ type ProcessReportHistoryEntry struct {
} }
func (ProcessReportHistoryEntry) IsIProcessReportHistoryEntry() {} func (ProcessReportHistoryEntry) IsIProcessReportHistoryEntry() {}
func (this ProcessReportHistoryEntry) GetCreatedAt() time.Time { return this.CreatedAt }
func (this ProcessReportHistoryEntry) GetPrelude() []string {
if this.Prelude == nil {
return nil
}
interfaceSlice := make([]string, 0, len(this.Prelude))
for _, concrete := range this.Prelude {
interfaceSlice = append(interfaceSlice, concrete)
}
return interfaceSlice
}
func (this ProcessReportHistoryEntry) GetLog() []*ProcessReportLogEntry {
if this.Log == nil {
return nil
}
interfaceSlice := make([]*ProcessReportLogEntry, 0, len(this.Log))
for _, concrete := range this.Log {
interfaceSlice = append(interfaceSlice, concrete)
}
return interfaceSlice
}
type ProcessReportLogEntry struct { type ProcessReportLogEntry struct {
Timestamp time.Time `json:"timestamp"` Timestamp time.Time `json:"timestamp"`

View File

@@ -1,5 +1,5 @@
extend type Query { extend type Query {
playoutStatus(id: ID!, input: ID!): RawAVstream playoutStatus(id: ID!, group: String, input: ID!): RawAVstream
} }
type RawAVstreamIO { type RawAVstreamIO {

View File

@@ -1,7 +1,7 @@
extend type Query { extend type Query {
processes: [Process!]! processes(idpattern: String, refpattern: String, group: String): [Process!]!
process(id: ID!): Process process(id: ID!, group: String): Process
probe(id: ID!): Probe! probe(id: ID!, group: String): Probe!
} }
type ProcessConfigIO { type ProcessConfigIO {
@@ -18,6 +18,8 @@ type ProcessConfigLimits {
type ProcessConfig { type ProcessConfig {
id: String! id: String!
owner: String!
group: String!
type: String! type: String!
reference: String! reference: String!
input: [ProcessConfigIO!]! input: [ProcessConfigIO!]!
@@ -68,6 +70,8 @@ type ProcessReport implements IProcessReportHistoryEntry {
type Process { type Process {
id: String! id: String!
owner: String!
group: String!
type: String! type: String!
reference: String! reference: String!
created_at: Time! created_at: Time!

View File

@@ -12,6 +12,7 @@ import (
"github.com/datarhei/core/v16/http/graph/scalars" "github.com/datarhei/core/v16/http/graph/scalars"
) )
// About is the resolver for the about field.
func (r *queryResolver) About(ctx context.Context) (*models.About, error) { func (r *queryResolver) About(ctx context.Context) (*models.About, error) {
createdAt := r.Restream.CreatedAt() createdAt := r.Restream.CreatedAt()

View File

@@ -10,6 +10,7 @@ import (
"github.com/datarhei/core/v16/log" "github.com/datarhei/core/v16/log"
) )
// Log is the resolver for the log field.
func (r *queryResolver) Log(ctx context.Context) ([]string, error) { func (r *queryResolver) Log(ctx context.Context) ([]string, error) {
if r.LogBuffer == nil { if r.LogBuffer == nil {
r.LogBuffer = log.NewBufferWriter(log.Lsilent, 1) r.LogBuffer = log.NewBufferWriter(log.Lsilent, 1)

View File

@@ -12,6 +12,7 @@ import (
"github.com/datarhei/core/v16/monitor/metric" "github.com/datarhei/core/v16/monitor/metric"
) )
// Metrics is the resolver for the metrics field.
func (r *queryResolver) Metrics(ctx context.Context, query models.MetricsInput) (*models.Metrics, error) { func (r *queryResolver) Metrics(ctx context.Context, query models.MetricsInput) (*models.Metrics, error) {
patterns := []metric.Pattern{} patterns := []metric.Pattern{}

View File

@@ -13,8 +13,11 @@ import (
"github.com/datarhei/core/v16/playout" "github.com/datarhei/core/v16/playout"
) )
func (r *queryResolver) PlayoutStatus(ctx context.Context, id string, input string) (*models.RawAVstream, error) { // PlayoutStatus is the resolver for the playoutStatus field.
addr, err := r.Restream.GetPlayout(id, input) func (r *queryResolver) PlayoutStatus(ctx context.Context, id string, group *string, input string) (*models.RawAVstream, error) {
user, _ := ctx.Value("user").(string)
addr, err := r.Restream.GetPlayout(id, user, *group, input)
if err != nil { if err != nil {
return nil, fmt.Errorf("unknown process or input: %w", err) return nil, fmt.Errorf("unknown process or input: %w", err)
} }

View File

@@ -9,13 +9,15 @@ import (
"github.com/datarhei/core/v16/http/graph/models" "github.com/datarhei/core/v16/http/graph/models"
) )
func (r *queryResolver) Processes(ctx context.Context) ([]*models.Process, error) { // Processes is the resolver for the processes field.
ids := r.Restream.GetProcessIDs("", "") func (r *queryResolver) Processes(ctx context.Context, idpattern *string, refpattern *string, group *string) ([]*models.Process, error) {
user, _ := ctx.Value("user").(string)
ids := r.Restream.GetProcessIDs(*idpattern, *refpattern, user, *group)
procs := []*models.Process{} procs := []*models.Process{}
for _, id := range ids { for _, id := range ids {
p, err := r.getProcess(id) p, err := r.getProcess(id, user, *group)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -26,12 +28,18 @@ func (r *queryResolver) Processes(ctx context.Context) ([]*models.Process, error
return procs, nil return procs, nil
} }
func (r *queryResolver) Process(ctx context.Context, id string) (*models.Process, error) { // Process is the resolver for the process field.
return r.getProcess(id) func (r *queryResolver) Process(ctx context.Context, id string, group *string) (*models.Process, error) {
user, _ := ctx.Value("user").(string)
return r.getProcess(id, user, *group)
} }
func (r *queryResolver) Probe(ctx context.Context, id string) (*models.Probe, error) { // Probe is the resolver for the probe field.
probe := r.Restream.Probe(id) func (r *queryResolver) Probe(ctx context.Context, id string, group *string) (*models.Probe, error) {
user, _ := ctx.Value("user").(string)
probe := r.Restream.Probe(id, user, *group)
p := &models.Probe{} p := &models.Probe{}
p.UnmarshalRestream(probe) p.UnmarshalRestream(probe)

View File

@@ -22,23 +22,23 @@ type Resolver struct {
LogBuffer log.BufferWriter LogBuffer log.BufferWriter
} }
func (r *queryResolver) getProcess(id string) (*models.Process, error) { func (r *queryResolver) getProcess(id, user, group string) (*models.Process, error) {
process, err := r.Restream.GetProcess(id) process, err := r.Restream.GetProcess(id, user, group)
if err != nil { if err != nil {
return nil, err return nil, err
} }
state, err := r.Restream.GetProcessState(id) state, err := r.Restream.GetProcessState(id, user, group)
if err != nil { if err != nil {
return nil, err return nil, err
} }
report, err := r.Restream.GetProcessLog(id) report, err := r.Restream.GetProcessLog(id, user, group)
if err != nil { if err != nil {
return nil, err return nil, err
} }
m, err := r.Restream.GetProcessMetadata(id, "") m, err := r.Restream.GetProcessMetadata(id, user, group, "")
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -9,10 +9,12 @@ import (
"github.com/datarhei/core/v16/http/graph/graph" "github.com/datarhei/core/v16/http/graph/graph"
) )
// Ping is the resolver for the ping field.
func (r *mutationResolver) Ping(ctx context.Context) (string, error) { func (r *mutationResolver) Ping(ctx context.Context) (string, error) {
return "pong", nil return "pong", nil
} }
// Ping is the resolver for the ping field.
func (r *queryResolver) Ping(ctx context.Context) (string, error) { func (r *queryResolver) Ping(ctx context.Context) (string, error) {
return "pong", nil return "pong", nil
} }

View File

@@ -1,6 +1,7 @@
package api package api
import ( import (
"context"
"net/http" "net/http"
"github.com/datarhei/core/v16/http/graph/graph" "github.com/datarhei/core/v16/http/graph/graph"
@@ -18,7 +19,7 @@ type GraphHandler struct {
playgroundHandler http.HandlerFunc playgroundHandler http.HandlerFunc
} }
// NewRestream return a new Restream type. You have to provide a valid Restreamer instance. // NewGraph return a new GraphHandler type. You have to provide a valid Restreamer instance.
func NewGraph(resolver resolver.Resolver, path string) *GraphHandler { func NewGraph(resolver resolver.Resolver, path string) *GraphHandler {
g := &GraphHandler{ g := &GraphHandler{
resolver: resolver, resolver: resolver,
@@ -43,7 +44,12 @@ func NewGraph(resolver resolver.Resolver, path string) *GraphHandler {
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Router /api/graph/query [post] // @Router /api/graph/query [post]
func (g *GraphHandler) Query(c echo.Context) error { func (g *GraphHandler) Query(c echo.Context) error {
g.queryHandler.ServeHTTP(c.Response(), c.Request()) user, _ := c.Get("user").(string)
r := c.Request()
ctx := context.WithValue(r.Context(), "user", user)
g.queryHandler.ServeHTTP(c.Response(), r.WithContext(ctx))
return nil return nil
} }

View File

@@ -44,8 +44,10 @@ func NewPlayout(restream restream.Restreamer) *PlayoutHandler {
func (h *PlayoutHandler) Status(c echo.Context) error { func (h *PlayoutHandler) Status(c echo.Context) error {
id := util.PathParam(c, "id") id := util.PathParam(c, "id")
inputid := util.PathParam(c, "inputid") inputid := util.PathParam(c, "inputid")
user := util.DefaultContext(c, "user", "")
group := util.DefaultQuery(c, "group", "")
addr, err := h.restream.GetPlayout(id, inputid) addr, err := h.restream.GetPlayout(id, user, group, inputid)
if err != nil { if err != nil {
return api.Err(http.StatusNotFound, "Unknown process or input", "%s", err) return api.Err(http.StatusNotFound, "Unknown process or input", "%s", err)
} }
@@ -102,8 +104,10 @@ func (h *PlayoutHandler) Keyframe(c echo.Context) error {
id := util.PathParam(c, "id") id := util.PathParam(c, "id")
inputid := util.PathParam(c, "inputid") inputid := util.PathParam(c, "inputid")
name := util.PathWildcardParam(c) name := util.PathWildcardParam(c)
user := util.DefaultContext(c, "user", "")
group := util.DefaultQuery(c, "group", "")
addr, err := h.restream.GetPlayout(id, inputid) addr, err := h.restream.GetPlayout(id, user, group, inputid)
if err != nil { if err != nil {
return api.Err(http.StatusNotFound, "Unknown process or input", "%s", err) return api.Err(http.StatusNotFound, "Unknown process or input", "%s", err)
} }
@@ -149,8 +153,10 @@ func (h *PlayoutHandler) Keyframe(c echo.Context) error {
func (h *PlayoutHandler) EncodeErrorframe(c echo.Context) error { func (h *PlayoutHandler) EncodeErrorframe(c echo.Context) error {
id := util.PathParam(c, "id") id := util.PathParam(c, "id")
inputid := util.PathParam(c, "inputid") inputid := util.PathParam(c, "inputid")
user := util.DefaultContext(c, "user", "")
group := util.DefaultQuery(c, "group", "")
addr, err := h.restream.GetPlayout(id, inputid) addr, err := h.restream.GetPlayout(id, user, group, inputid)
if err != nil { if err != nil {
return api.Err(http.StatusNotFound, "Unknown process or input", "%s", err) return api.Err(http.StatusNotFound, "Unknown process or input", "%s", err)
} }
@@ -193,8 +199,10 @@ func (h *PlayoutHandler) EncodeErrorframe(c echo.Context) error {
func (h *PlayoutHandler) SetErrorframe(c echo.Context) error { func (h *PlayoutHandler) SetErrorframe(c echo.Context) error {
id := util.PathParam(c, "id") id := util.PathParam(c, "id")
inputid := util.PathParam(c, "inputid") inputid := util.PathParam(c, "inputid")
user := util.DefaultContext(c, "user", "")
group := util.DefaultQuery(c, "group", "")
addr, err := h.restream.GetPlayout(id, inputid) addr, err := h.restream.GetPlayout(id, user, group, inputid)
if err != nil { if err != nil {
return api.Err(http.StatusNotFound, "Unknown process or input", "%s", err) return api.Err(http.StatusNotFound, "Unknown process or input", "%s", err)
} }
@@ -238,8 +246,10 @@ func (h *PlayoutHandler) SetErrorframe(c echo.Context) error {
func (h *PlayoutHandler) ReopenInput(c echo.Context) error { func (h *PlayoutHandler) ReopenInput(c echo.Context) error {
id := util.PathParam(c, "id") id := util.PathParam(c, "id")
inputid := util.PathParam(c, "inputid") inputid := util.PathParam(c, "inputid")
user := util.DefaultContext(c, "user", "")
group := util.DefaultQuery(c, "group", "")
addr, err := h.restream.GetPlayout(id, inputid) addr, err := h.restream.GetPlayout(id, user, group, inputid)
if err != nil { if err != nil {
return api.Err(http.StatusNotFound, "Unknown process or input", "%s", err) return api.Err(http.StatusNotFound, "Unknown process or input", "%s", err)
} }
@@ -281,8 +291,10 @@ func (h *PlayoutHandler) ReopenInput(c echo.Context) error {
func (h *PlayoutHandler) SetStream(c echo.Context) error { func (h *PlayoutHandler) SetStream(c echo.Context) error {
id := util.PathParam(c, "id") id := util.PathParam(c, "id")
inputid := util.PathParam(c, "inputid") inputid := util.PathParam(c, "inputid")
user := util.DefaultContext(c, "user", "")
group := util.DefaultQuery(c, "group", "")
addr, err := h.restream.GetPlayout(id, inputid) addr, err := h.restream.GetPlayout(id, user, group, inputid)
if err != nil { if err != nil {
return api.Err(http.StatusNotFound, "Unknown process or input", "%s", err) return api.Err(http.StatusNotFound, "Unknown process or input", "%s", err)
} }

View File

@@ -37,6 +37,8 @@ func NewRestream(restream restream.Restreamer) *RestreamHandler {
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Router /api/v3/process [post] // @Router /api/v3/process [post]
func (h *RestreamHandler) Add(c echo.Context) error { func (h *RestreamHandler) Add(c echo.Context) error {
user := util.DefaultContext(c, "user", "")
process := api.ProcessConfig{ process := api.ProcessConfig{
ID: shortuuid.New(), ID: shortuuid.New(),
Type: "ffmpeg", Type: "ffmpeg",
@@ -56,6 +58,7 @@ func (h *RestreamHandler) Add(c echo.Context) error {
} }
config := process.Marshal() config := process.Marshal()
config.Owner = user
if err := h.restream.AddProcess(config); err != nil { if err := h.restream.AddProcess(config); err != nil {
return api.Err(http.StatusBadRequest, "Invalid process config", "%s", err.Error()) return api.Err(http.StatusBadRequest, "Invalid process config", "%s", err.Error())
@@ -210,6 +213,7 @@ func (h *RestreamHandler) Update(c echo.Context) error {
} }
config := process.Marshal() config := process.Marshal()
config.Owner = user
if err := h.restream.UpdateProcess(id, user, group, config); err != nil { if err := h.restream.UpdateProcess(id, user, group, config); err != nil {
if err == restream.ErrUnknownProcess { if err == restream.ErrUnknownProcess {

View File

@@ -8,9 +8,9 @@ import (
"github.com/datarhei/core/v16/http/api" "github.com/datarhei/core/v16/http/api"
"github.com/datarhei/core/v16/http/mock" "github.com/datarhei/core/v16/http/mock"
"github.com/stretchr/testify/require"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
"github.com/stretchr/testify/require"
) )
type Response struct { type Response struct {

View File

@@ -43,17 +43,18 @@ func NewWidget(config WidgetConfig) *WidgetHandler {
// @Router /api/v3/widget/process/{id} [get] // @Router /api/v3/widget/process/{id} [get]
func (w *WidgetHandler) Get(c echo.Context) error { func (w *WidgetHandler) Get(c echo.Context) error {
id := util.PathParam(c, "id") id := util.PathParam(c, "id")
group := util.DefaultQuery(c, "group", "")
if w.restream == nil { if w.restream == nil {
return api.Err(http.StatusNotFound, "Unknown process ID") return api.Err(http.StatusNotFound, "Unknown process ID")
} }
process, err := w.restream.GetProcess(id) process, err := w.restream.GetProcess(id, "", group)
if err != nil { if err != nil {
return api.Err(http.StatusNotFound, "Unknown process ID", "%s", err) return api.Err(http.StatusNotFound, "Unknown process ID", "%s", err)
} }
state, err := w.restream.GetProcessState(id) state, err := w.restream.GetProcessState(id, "", group)
if err != nil { if err != nil {
return api.Err(http.StatusNotFound, "Unknown process ID", "%s", err) return api.Err(http.StatusNotFound, "Unknown process ID", "%s", err)
} }

View File

@@ -177,17 +177,13 @@ func NewWithConfig(config Config) echo.MiddlewareFunc {
c.Set("user", username) c.Set("user", username)
if identity != nil && identity.IsSuperuser() {
username = "$superuser"
}
if len(domain) == 0 { if len(domain) == 0 {
domain = "$none" domain = "$none"
} }
action := c.Request().Method action := c.Request().Method
if ok, _ := config.IAM.Enforce(username, domain, resource, action); !ok { if !config.IAM.Enforce(username, domain, resource, action) {
return api.Err(http.StatusForbidden, "Forbidden", "access denied") return api.Err(http.StatusForbidden, "Forbidden", "access denied")
} }

View File

@@ -16,6 +16,7 @@ import (
"github.com/datarhei/core/v16/http/api" "github.com/datarhei/core/v16/http/api"
"github.com/datarhei/core/v16/http/errorhandler" "github.com/datarhei/core/v16/http/errorhandler"
"github.com/datarhei/core/v16/http/validator" "github.com/datarhei/core/v16/http/validator"
"github.com/datarhei/core/v16/iam"
"github.com/datarhei/core/v16/internal/testhelper" "github.com/datarhei/core/v16/internal/testhelper"
"github.com/datarhei/core/v16/io/fs" "github.com/datarhei/core/v16/io/fs"
"github.com/datarhei/core/v16/restream" "github.com/datarhei/core/v16/restream"
@@ -52,9 +53,23 @@ func DummyRestreamer(pathPrefix string) (restream.Restreamer, error) {
return nil, err return nil, err
} }
iam, err := iam.NewIAM(iam.Config{
FS: memfs,
Superuser: iam.User{
Name: "foobar",
},
JWTRealm: "",
JWTSecret: "",
Logger: nil,
})
iam.AddPolicy("$anon", "$none", "api:/**", "ANY")
iam.AddPolicy("$anon", "$none", "fs:/**", "ANY")
rs, err := restream.New(restream.Config{ rs, err := restream.New(restream.Config{
Store: store, Store: store,
FFmpeg: ffmpeg, FFmpeg: ffmpeg,
IAM: iam,
}) })
if err != nil { if err != nil {
return nil, err return nil, err

View File

@@ -108,20 +108,7 @@ func (am *access) HasGroup(name string) bool {
} }
func (am *access) Enforce(name, domain, resource, action string) (bool, string) { func (am *access) Enforce(name, domain, resource, action string) (bool, string) {
l := am.logger.Debug().WithFields(log.Fields{
"subject": name,
"domain": domain,
"resource": resource,
"action": action,
})
ok, rule, _ := am.enforcer.EnforceEx(name, domain, resource, action) ok, rule, _ := am.enforcer.EnforceEx(name, domain, resource, action)
if !ok {
l.Log("no match")
} else {
l.WithField("rule", strings.Join(rule, ", ")).Log("match")
}
return ok, strings.Join(rule, ", ") return ok, strings.Join(rule, ", ")
} }

View File

@@ -13,21 +13,11 @@ func resourceMatch(request, domain, policy string) bool {
if reqPrefix != polPrefix { if reqPrefix != polPrefix {
return false return false
} }
/*
fmt.Printf("prefix: %s\n", reqPrefix)
fmt.Printf("requested resource: %s\n", reqResource)
fmt.Printf("requested domain: %s\n", domain)
fmt.Printf("policy resource: %s\n", polResource)
*/
var match bool var match bool
var err error var err error
if reqPrefix == "processid" { if reqPrefix == "api" {
match, err = globMatch(polResource, reqResource)
if err != nil {
return false
}
} else if reqPrefix == "api" {
match, err = globMatch(polResource, reqResource, rune('/')) match, err = globMatch(polResource, reqResource, rune('/'))
if err != nil { if err != nil {
return false return false
@@ -38,12 +28,12 @@ func resourceMatch(request, domain, policy string) bool {
return false return false
} }
} else if reqPrefix == "rtmp" { } else if reqPrefix == "rtmp" {
match, err = globMatch(polResource, reqResource) match, err = globMatch(polResource, reqResource, rune('/'))
if err != nil { if err != nil {
return false return false
} }
} else if reqPrefix == "srt" { } else if reqPrefix == "srt" {
match, err = globMatch(polResource, reqResource) match, err = globMatch(polResource, reqResource, rune('/'))
if err != nil { if err != nil {
return false return false
} }
@@ -54,8 +44,6 @@ func resourceMatch(request, domain, policy string) bool {
} }
} }
//fmt.Printf("match: %v\n", match)
return match return match
} }
@@ -74,6 +62,10 @@ func actionMatch(request string, policy string) bool {
return false return false
} }
if len(actions) == 1 && actions[0] == "ANY" {
return true
}
for _, a := range actions { for _, a := range actions {
if request == a { if request == a {
return true return true

View File

@@ -6,7 +6,7 @@ import (
) )
type IAM interface { type IAM interface {
Enforce(user, domain, resource, action string) (bool, string) Enforce(user, domain, resource, action string) bool
IsDomain(domain string) bool IsDomain(domain string) bool
AddPolicy(username, domain, resource, actions string) bool AddPolicy(username, domain, resource, actions string) bool
@@ -26,6 +26,8 @@ type IAM interface {
type iam struct { type iam struct {
im IdentityManager im IdentityManager
am AccessManager am AccessManager
logger log.Logger
} }
type Config struct { type Config struct {
@@ -56,10 +58,17 @@ func NewIAM(config Config) (IAM, error) {
return nil, err return nil, err
} }
return &iam{ iam := &iam{
im: im, im: im,
am: am, am: am,
}, nil logger: config.Logger,
}
if iam.logger == nil {
iam.logger = log.New("")
}
return iam, nil
} }
func (i *iam) Close() { func (i *iam) Close() {
@@ -67,12 +76,38 @@ func (i *iam) Close() {
i.im = nil i.im = nil
i.am = nil i.am = nil
return
} }
func (i *iam) Enforce(user, domain, resource, action string) (bool, string) { func (i *iam) Enforce(user, domain, resource, action string) bool {
return i.am.Enforce(user, domain, resource, action) superuser := false
if identity, err := i.im.GetVerifier(user); err == nil {
if identity.IsSuperuser() {
superuser = true
}
}
l := i.logger.Debug().WithFields(log.Fields{
"subject": user,
"domain": domain,
"resource": resource,
"action": action,
"superuser": superuser,
})
if superuser {
user = "$superuser"
}
ok, rule := i.am.Enforce(user, domain, resource, action)
if !ok {
l.Log("no match")
} else {
l.WithField("rule", rule).Log("match")
}
return ok
} }
func (i *iam) GetIdentity(name string) (IdentityVerifier, error) { func (i *iam) GetIdentity(name string) (IdentityVerifier, error) {

View File

@@ -86,6 +86,23 @@ func (u *User) marshalIdentity() *identity {
return i return i
} }
type IdentityVerifier interface {
Name() string
VerifyJWT(jwt string) (bool, error)
VerifyAPIPassword(password string) (bool, error)
VerifyAPIAuth0(jwt string) (bool, error)
VerifyServiceBasicAuth(password string) (bool, error)
VerifyServiceToken(token string) (bool, error)
GetServiceBasicAuth() string
GetServiceToken() string
IsSuperuser() bool
}
type identity struct { type identity struct {
user User user User
@@ -269,6 +286,21 @@ func (i *identity) VerifyServiceBasicAuth(password string) (bool, error) {
return i.user.Auth.Services.Basic.Password == password, nil return i.user.Auth.Services.Basic.Password == password, nil
} }
func (i *identity) GetServiceBasicAuth() string {
i.lock.RLock()
defer i.lock.RUnlock()
if !i.isValid() {
return ""
}
if !i.user.Auth.Services.Basic.Enable {
return ""
}
return i.user.Auth.Services.Basic.Password
}
func (i *identity) VerifyServiceToken(token string) (bool, error) { func (i *identity) VerifyServiceToken(token string) (bool, error) {
i.lock.RLock() i.lock.RLock()
defer i.lock.RUnlock() defer i.lock.RUnlock()
@@ -286,6 +318,21 @@ func (i *identity) VerifyServiceToken(token string) (bool, error) {
return false, nil return false, nil
} }
func (i *identity) GetServiceToken() string {
i.lock.RLock()
defer i.lock.RUnlock()
if !i.isValid() {
return ""
}
if len(i.user.Auth.Services.Token) == 0 {
return ""
}
return i.Name() + ":" + i.user.Auth.Services.Token[0]
}
func (i *identity) isValid() bool { func (i *identity) isValid() bool {
return i.valid return i.valid
} }
@@ -297,24 +344,9 @@ func (i *identity) IsSuperuser() bool {
return i.user.Superuser return i.user.Superuser
} }
type IdentityVerifier interface {
Name() string
VerifyJWT(jwt string) (bool, error)
VerifyAPIPassword(password string) (bool, error)
VerifyAPIAuth0(jwt string) (bool, error)
VerifyServiceBasicAuth(password string) (bool, error)
VerifyServiceToken(token string) (bool, error)
IsSuperuser() bool
}
type IdentityManager interface { type IdentityManager interface {
Create(identity User) error Create(identity User) error
Remove(name string) error Remove(name string) error
Get(name string) (User, error)
GetVerifier(name string) (IdentityVerifier, error) GetVerifier(name string) (IdentityVerifier, error)
GetVerifierByAuth0(name string) (IdentityVerifier, error) GetVerifierByAuth0(name string) (IdentityVerifier, error)
GetDefaultVerifier() (IdentityVerifier, error) GetDefaultVerifier() (IdentityVerifier, error)
@@ -404,8 +436,6 @@ func (im *identityManager) Close() {
} }
im.tenants = map[string]*auth0Tenant{} im.tenants = map[string]*auth0Tenant{}
return
} }
func (im *identityManager) Create(u User) error { func (im *identityManager) Create(u User) error {
@@ -485,7 +515,7 @@ func (im *identityManager) getIdentity(name string) (*identity, error) {
if im.root.user.Name == name { if im.root.user.Name == name {
identity = im.root identity = im.root
} else { } else {
identity, _ = im.identities[name] identity = im.identities[name]
} }
@@ -499,18 +529,6 @@ func (im *identityManager) getIdentity(name string) (*identity, error) {
return identity, nil return identity, nil
} }
func (im *identityManager) Get(name string) (User, error) {
im.lock.RLock()
defer im.lock.RUnlock()
identity, err := im.getIdentity(name)
if err != nil {
return User{}, fmt.Errorf("not found")
}
return identity.user, nil
}
func (im *identityManager) GetVerifier(name string) (IdentityVerifier, error) { func (im *identityManager) GetVerifier(name string) (IdentityVerifier, error) {
im.lock.RLock() im.lock.RLock()
defer im.lock.RUnlock() defer im.lock.RUnlock()

1
iam/identity_test.go Normal file
View File

@@ -0,0 +1 @@
package iam

View File

@@ -18,8 +18,6 @@ func Validate(address string) error {
// Parse parses an URL into its components. Returns a net/url.URL or // Parse parses an URL into its components. Returns a net/url.URL or
// an error if the URL couldn't be parsed. // an error if the URL couldn't be parsed.
func Parse(address string) (*url.URL, error) { func Parse(address string) (*url.URL, error) {
address = reScheme.ReplaceAllString(address, "//")
u, err := url.Parse(address) u, err := url.Parse(address)
return u, err return u, err

View File

@@ -24,6 +24,7 @@ import (
"github.com/datarhei/core/v16/restream/app" "github.com/datarhei/core/v16/restream/app"
rfs "github.com/datarhei/core/v16/restream/fs" rfs "github.com/datarhei/core/v16/restream/fs"
"github.com/datarhei/core/v16/restream/replace" "github.com/datarhei/core/v16/restream/replace"
"github.com/datarhei/core/v16/restream/rewrite"
"github.com/datarhei/core/v16/restream/store" "github.com/datarhei/core/v16/restream/store"
"github.com/Masterminds/semver/v3" "github.com/Masterminds/semver/v3"
@@ -67,6 +68,7 @@ type Config struct {
Store store.Store Store store.Store
Filesystems []fs.Filesystem Filesystems []fs.Filesystem
Replace replace.Replacer Replace replace.Replacer
Rewrite rewrite.Rewriter
FFmpeg ffmpeg.FFmpeg FFmpeg ffmpeg.FFmpeg
MaxProcesses int64 MaxProcesses int64
Logger log.Logger Logger log.Logger
@@ -112,6 +114,7 @@ type restream struct {
stopObserver context.CancelFunc stopObserver context.CancelFunc
} }
replace replace.Replacer replace replace.Replacer
rewrite rewrite.Rewriter
tasks map[string]*task tasks map[string]*task
logger log.Logger logger log.Logger
metadata map[string]interface{} metadata map[string]interface{}
@@ -132,6 +135,7 @@ func New(config Config) (Restreamer, error) {
createdAt: time.Now(), createdAt: time.Now(),
store: config.Store, store: config.Store,
replace: config.Replace, replace: config.Replace,
rewrite: config.Rewrite,
logger: config.Logger, logger: config.Logger,
iam: config.IAM, iam: config.IAM,
} }
@@ -418,16 +422,21 @@ func (r *restream) save() {
func (r *restream) enforce(name, group, processid, action string) bool { func (r *restream) enforce(name, group, processid, action string) bool {
if len(name) == 0 { if len(name) == 0 {
name = "$anon" // This is for backwards compatibility. Existing processes don't have an owner.
// All processes that will be added later will have an owner ($anon, ...).
identity, err := r.iam.GetDefaultIdentity()
if err != nil {
name = "$anon"
} else {
name = identity.Name()
}
} }
if len(group) == 0 { if len(group) == 0 {
group = "$none" group = "$none"
} }
ok, _ := r.iam.Enforce(name, group, "process:"+processid, action) return r.iam.Enforce(name, group, "process:"+processid, action)
return ok
} }
func (r *restream) ID() string { func (r *restream) ID() string {
@@ -878,37 +887,138 @@ func (r *restream) resolveAddresses(tasks map[string]*task, config *app.Config)
} }
func (r *restream) resolveAddress(tasks map[string]*task, id, address string) (string, error) { func (r *restream) resolveAddress(tasks map[string]*task, id, address string) (string, error) {
re := regexp.MustCompile(`^#(.+):output=(.+)`) matches, err := parseAddressReference(address)
if err != nil {
if len(address) == 0 { return address, err
return address, fmt.Errorf("empty address")
} }
if address[0] != '#' { // Address is not a reference
if _, ok := matches["address"]; ok {
return address, nil return address, nil
} }
matches := re.FindStringSubmatch(address) if matches["id"] == id {
if matches == nil { return address, fmt.Errorf("self-reference is not allowed (%s)", address)
return address, fmt.Errorf("invalid format (%s)", address)
} }
if matches[1] == id { var t *task = nil
return address, fmt.Errorf("self-reference not possible (%s)", address)
}
task, ok := tasks[matches[1]] for _, tsk := range tasks {
if !ok { if tsk.id == matches["id"] && tsk.group == matches["group"] {
return address, fmt.Errorf("unknown process '%s' (%s)", matches[1], address) t = tsk
} break
for _, x := range task.config.Output {
if x.ID == matches[2] {
return x.Address, nil
} }
} }
return address, fmt.Errorf("the process '%s' has no outputs with the ID '%s' (%s)", matches[1], matches[2], address) if t == nil {
return address, fmt.Errorf("unknown process '%s' in group '%s' (%s)", matches["id"], matches["group"], address)
}
identity, _ := r.iam.GetIdentity(t.config.Owner)
teeOptions := regexp.MustCompile(`^\[[^\]]*\]`)
for _, x := range t.config.Output {
if x.ID != matches["output"] {
continue
}
// Check for non-tee output
if !strings.Contains(x.Address, "|") && !strings.HasPrefix(x.Address, "[") {
return r.rewrite.RewriteAddress(x.Address, identity, rewrite.READ), nil
}
// Split tee output in its individual addresses
addresses := strings.Split(x.Address, "|")
if len(addresses) == 0 {
return x.Address, nil
}
// Remove tee options
for i, a := range addresses {
addresses[i] = teeOptions.ReplaceAllString(a, "")
}
if len(matches["source"]) == 0 {
return r.rewrite.RewriteAddress(addresses[0], identity, rewrite.READ), nil
}
for _, a := range addresses {
u, err := url.Parse(a)
if err != nil {
// Ignore invalid addresses
continue
}
if matches["source"] == "hls" {
if (u.Scheme == "http" || u.Scheme == "https") && strings.HasSuffix(u.Path, ".m3u8") {
return r.rewrite.RewriteAddress(a, identity, rewrite.READ), nil
}
} else if matches["source"] == "rtmp" {
if u.Scheme == "rtmp" {
return r.rewrite.RewriteAddress(a, identity, rewrite.READ), nil
}
} else if matches["source"] == "srt" {
if u.Scheme == "srt" {
return r.rewrite.RewriteAddress(a, identity, rewrite.READ), nil
}
}
}
// If none of the sources matched, return the first address
return r.rewrite.RewriteAddress(addresses[0], identity, rewrite.READ), nil
}
return address, fmt.Errorf("the process '%s' in group '%s' has no outputs with the ID '%s' (%s)", matches["id"], matches["group"], matches["output"], address)
}
func parseAddressReference(address string) (map[string]string, error) {
if len(address) == 0 {
return nil, fmt.Errorf("empty address")
}
if address[0] != '#' {
return map[string]string{
"address": address,
}, nil
}
re := regexp.MustCompile(`:(output|group|source)=(.+)`)
results := map[string]string{}
idEnd := -1
value := address
key := ""
for {
matches := re.FindStringSubmatchIndex(value)
if matches == nil {
break
}
if idEnd < 0 {
idEnd = matches[2] - 1
}
if len(key) != 0 {
results[key] = value[:matches[2]-1]
}
key = value[matches[2]:matches[3]]
value = value[matches[4]:matches[5]]
results[key] = value
}
if idEnd < 0 {
return nil, fmt.Errorf("invalid format (%s)", address)
}
results["id"] = address[1:idEnd]
return results, nil
} }
func (r *restream) UpdateProcess(id, user, group string, config *app.Config) error { func (r *restream) UpdateProcess(id, user, group string, config *app.Config) error {
@@ -1661,6 +1771,7 @@ func (r *restream) GetMetadata(key string) (interface{}, error) {
func resolvePlaceholders(config *app.Config, r replace.Replacer) { func resolvePlaceholders(config *app.Config, r replace.Replacer) {
vars := map[string]string{ vars := map[string]string{
"processid": config.ID, "processid": config.ID,
"owner": config.Owner,
"reference": config.Reference, "reference": config.Reference,
"group": config.Group, "group": config.Group,
} }

View File

@@ -12,6 +12,7 @@ import (
"github.com/datarhei/core/v16/net" "github.com/datarhei/core/v16/net"
"github.com/datarhei/core/v16/restream/app" "github.com/datarhei/core/v16/restream/app"
"github.com/datarhei/core/v16/restream/replace" "github.com/datarhei/core/v16/restream/replace"
"github.com/datarhei/core/v16/restream/rewrite"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@@ -49,9 +50,15 @@ func getDummyRestreamer(portrange net.Portranger, validatorIn, validatorOut ffmp
iam.AddPolicy("$anon", "$none", "process:*", "CREATE|GET|DELETE|UPDATE|COMMAND|PROBE|METADATA|PLAYOUT") iam.AddPolicy("$anon", "$none", "process:*", "CREATE|GET|DELETE|UPDATE|COMMAND|PROBE|METADATA|PLAYOUT")
rewriter, err := rewrite.New(rewrite.Config{})
if err != nil {
return nil, err
}
rs, err := New(Config{ rs, err := New(Config{
FFmpeg: ffmpeg, FFmpeg: ffmpeg,
Replace: replacer, Replace: replacer,
Rewrite: rewriter,
IAM: iam, IAM: iam,
}) })
if err != nil { if err != nil {
@@ -528,6 +535,39 @@ func TestPlayoutRange(t *testing.T) {
require.Equal(t, "127.0.0.1:3000", addr, "the playout address should be 127.0.0.1:3000") require.Equal(t, "127.0.0.1:3000", addr, "the playout address should be 127.0.0.1:3000")
} }
func TestParseAddressReference(t *testing.T) {
matches, err := parseAddressReference("foobar")
require.NoError(t, err)
require.Equal(t, "foobar", matches["address"])
_, err = parseAddressReference("#foobar")
require.Error(t, err)
_, err = parseAddressReference("#foobar:nothing=foo")
require.Error(t, err)
matches, err = parseAddressReference("#foobar:output=foo")
require.NoError(t, err)
require.Equal(t, "foobar", matches["id"])
require.Equal(t, "foo", matches["output"])
matches, err = parseAddressReference("#foobar:group=foo")
require.NoError(t, err)
require.Equal(t, "foobar", matches["id"])
require.Equal(t, "foo", matches["group"])
matches, err = parseAddressReference("#foobar:nothing=foo:output=bar")
require.NoError(t, err)
require.Equal(t, "foobar:nothing=foo", matches["id"])
require.Equal(t, "bar", matches["output"])
matches, err = parseAddressReference("#foobar:output=foo:group=bar")
require.NoError(t, err)
require.Equal(t, "foobar", matches["id"])
require.Equal(t, "foo", matches["output"])
require.Equal(t, "bar", matches["group"])
}
func TestAddressReference(t *testing.T) { func TestAddressReference(t *testing.T) {
rs, err := getDummyRestreamer(nil, nil, nil, nil) rs, err := getDummyRestreamer(nil, nil, nil, nil)
require.NoError(t, err) require.NoError(t, err)
@@ -559,6 +599,44 @@ func TestAddressReference(t *testing.T) {
require.Equal(t, nil, err, "should resolve reference") require.Equal(t, nil, err, "should resolve reference")
} }
func TestTeeAddressReference(t *testing.T) {
rs, err := getDummyRestreamer(nil, nil, nil, nil)
require.NoError(t, err)
process1 := getDummyProcess()
process2 := getDummyProcess()
process3 := getDummyProcess()
process4 := getDummyProcess()
process1.Output[0].Address = "[f=hls]http://example.com/live.m3u8|[f=flv]rtmp://example.com/live.stream?token=123"
process2.ID = "process2"
process3.ID = "process3"
process4.ID = "process4"
rs.AddProcess(process1)
process2.Input[0].Address = "#process:output=out"
err = rs.AddProcess(process2)
require.Equal(t, nil, err, "should resolve reference")
process3.Input[0].Address = "#process:output=out:source=hls"
err = rs.AddProcess(process3)
require.Equal(t, nil, err, "should resolve reference")
process4.Input[0].Address = "#process:output=out:source=rtmp"
err = rs.AddProcess(process4)
require.Equal(t, nil, err, "should resolve reference")
r := rs.(*restream)
require.Equal(t, "http://example.com/live.m3u8", r.tasks["process2~"].config.Input[0].Address)
require.Equal(t, "http://example.com/live.m3u8", r.tasks["process3~"].config.Input[0].Address)
require.Equal(t, "rtmp://example.com/live.stream?token=123", r.tasks["process4~"].config.Input[0].Address)
}
func TestConfigValidation(t *testing.T) { func TestConfigValidation(t *testing.T) {
rsi, err := getDummyRestreamer(nil, nil, nil, nil) rsi, err := getDummyRestreamer(nil, nil, nil, nil)
require.NoError(t, err) require.NoError(t, err)
@@ -863,5 +941,8 @@ func TestReplacer(t *testing.T) {
StaleTimeout: 0, StaleTimeout: 0,
} }
require.Equal(t, process, rs.tasks["314159265359"].config) task, ok := rs.tasks["314159265359~"]
require.True(t, ok)
require.Equal(t, process, task.config)
} }

156
restream/rewrite/rewrite.go Normal file
View File

@@ -0,0 +1,156 @@
// Package rewrite provides facilities for rewriting a local HLS, RTMP, and SRT address.
package rewrite
import (
"fmt"
"net/url"
"github.com/datarhei/core/v16/iam"
"github.com/datarhei/core/v16/rtmp"
srturl "github.com/datarhei/core/v16/srt/url"
)
type Access string
var (
READ Access = "read"
WRITE Access = "write"
)
type Config struct {
HTTPBase string
RTMPBase string
SRTBase string
}
// to a new identity, i.e. adjusting the credentials to the given identity.
type Rewriter interface {
RewriteAddress(address string, identity iam.IdentityVerifier, mode Access) string
}
type rewrite struct {
httpBase string
rtmpBase string
srtBase string
}
func New(config Config) (Rewriter, error) {
r := &rewrite{
httpBase: config.HTTPBase,
rtmpBase: config.RTMPBase,
srtBase: config.SRTBase,
}
return r, nil
}
func (g *rewrite) RewriteAddress(address string, identity iam.IdentityVerifier, mode Access) string {
u, err := url.Parse(address)
if err != nil {
return address
}
// Decide whether this is our local server
if !g.isLocal(u) {
return address
}
if identity == nil {
return address
}
if u.Scheme == "http" || u.Scheme == "https" {
return g.httpURL(u, mode, identity)
} else if u.Scheme == "rtmp" {
return g.rtmpURL(u, mode, identity)
} else if u.Scheme == "srt" {
return g.srtURL(u, mode, identity)
}
return address
}
func (g *rewrite) isLocal(u *url.URL) bool {
var base *url.URL
var err error
if u.Scheme == "http" || u.Scheme == "https" {
base, err = url.Parse(g.httpBase)
} else if u.Scheme == "rtmp" {
base, err = url.Parse(g.rtmpBase)
} else if u.Scheme == "srt" {
base, err = url.Parse(g.srtBase)
} else {
err = fmt.Errorf("unsupported scheme")
}
if err != nil {
return false
}
hostname := u.Hostname()
port := u.Port()
if base.Hostname() == "localhost" {
if hostname != "localhost" && hostname != "127.0.0.1" && hostname != "::1" {
return false
}
hostname = "localhost"
}
host := hostname + ":" + port
return host == base.Host
}
func (g *rewrite) httpURL(u *url.URL, mode Access, identity iam.IdentityVerifier) string {
password := identity.GetServiceBasicAuth()
if len(password) == 0 {
u.User = nil
} else {
u.User = url.UserPassword(identity.Name(), password)
}
return u.String()
}
func (g *rewrite) rtmpURL(u *url.URL, mode Access, identity iam.IdentityVerifier) string {
token := identity.GetServiceToken()
// Remove the existing token from the path
path, _ := rtmp.GetToken(u)
u.Path = path
q := u.Query()
q.Set("token", token)
u.RawQuery = q.Encode()
return u.String()
}
func (g *rewrite) srtURL(u *url.URL, mode Access, identity iam.IdentityVerifier) string {
token := identity.GetServiceToken()
q := u.Query()
streamInfo, err := srturl.ParseStreamId(q.Get("streamid"))
if err != nil {
return u.String()
}
streamInfo.Token = token
if mode == WRITE {
streamInfo.Mode = "publish"
} else {
streamInfo.Mode = "request"
}
q.Set("streamid", streamInfo.String())
u.RawQuery = q.Encode()
return u.String()
}

View File

@@ -0,0 +1,156 @@
package rewrite
import (
"net/url"
"testing"
"github.com/datarhei/core/v16/iam"
"github.com/datarhei/core/v16/io/fs"
"github.com/stretchr/testify/require"
)
func getIdentityManager(enableBasic bool) iam.IdentityManager {
dummyfs, _ := fs.NewMemFilesystem(fs.MemConfig{})
im, _ := iam.NewIdentityManager(iam.IdentityConfig{
FS: dummyfs,
Superuser: iam.User{
Name: "foobar",
Superuser: false,
Auth: iam.UserAuth{
API: iam.UserAuthAPI{},
Services: iam.UserAuthServices{
Basic: iam.UserAuthPassword{
Enable: enableBasic,
Password: "basicauthpassword",
},
Token: []string{"servicetoken"},
},
},
},
JWTRealm: "",
JWTSecret: "",
Logger: nil,
})
return im
}
func TestRewriteHTTP(t *testing.T) {
im := getIdentityManager(false)
rewrite, err := New(Config{
HTTPBase: "http://localhost:8080/",
})
require.NoError(t, err)
require.NotNil(t, rewrite)
identity, err := im.GetVerifier("foobar")
require.NoError(t, err)
require.NotNil(t, identity)
samples := [][3]string{
{"http://example.com/live/stream.m3u8", "read", "http://example.com/live/stream.m3u8"},
{"http://example.com/live/stream.m3u8", "write", "http://example.com/live/stream.m3u8"},
{"http://localhost:8181/live/stream.m3u8", "read", "http://localhost:8181/live/stream.m3u8"},
{"http://localhost:8181/live/stream.m3u8", "write", "http://localhost:8181/live/stream.m3u8"},
{"http://localhost:8080/live/stream.m3u8", "read", "http://localhost:8080/live/stream.m3u8"},
{"http://localhost:8080/live/stream.m3u8", "write", "http://localhost:8080/live/stream.m3u8"},
{"http://admin:pass@localhost:8080/live/stream.m3u8", "read", "http://localhost:8080/live/stream.m3u8"},
{"http://admin:pass@localhost:8080/live/stream.m3u8", "write", "http://localhost:8080/live/stream.m3u8"},
}
for _, e := range samples {
rewritten := rewrite.RewriteAddress(e[0], identity, Access(e[1]))
require.Equal(t, e[2], rewritten, "%s %s", e[0], e[1])
}
}
func TestRewriteHTTPPassword(t *testing.T) {
im := getIdentityManager(true)
rewrite, err := New(Config{
HTTPBase: "http://localhost:8080/",
})
require.NoError(t, err)
require.NotNil(t, rewrite)
identity, err := im.GetVerifier("foobar")
require.NoError(t, err)
require.NotNil(t, identity)
samples := [][3]string{
{"http://example.com/live/stream.m3u8", "read", "http://example.com/live/stream.m3u8"},
{"http://example.com/live/stream.m3u8", "write", "http://example.com/live/stream.m3u8"},
{"http://localhost:8181/live/stream.m3u8", "read", "http://localhost:8181/live/stream.m3u8"},
{"http://localhost:8181/live/stream.m3u8", "write", "http://localhost:8181/live/stream.m3u8"},
{"http://localhost:8080/live/stream.m3u8", "read", "http://foobar:basicauthpassword@localhost:8080/live/stream.m3u8"},
{"http://localhost:8080/live/stream.m3u8", "write", "http://foobar:basicauthpassword@localhost:8080/live/stream.m3u8"},
{"http://admin:pass@localhost:8080/live/stream.m3u8", "read", "http://foobar:basicauthpassword@localhost:8080/live/stream.m3u8"},
{"http://admin:pass@localhost:8080/live/stream.m3u8", "write", "http://foobar:basicauthpassword@localhost:8080/live/stream.m3u8"},
}
for _, e := range samples {
rewritten := rewrite.RewriteAddress(e[0], identity, Access(e[1]))
require.Equal(t, e[2], rewritten, "%s %s", e[0], e[1])
}
}
func TestRewriteRTMP(t *testing.T) {
im := getIdentityManager(false)
rewrite, err := New(Config{
RTMPBase: "rtmp://localhost:1935/live",
})
require.NoError(t, err)
require.NotNil(t, rewrite)
identity, err := im.GetVerifier("foobar")
require.NoError(t, err)
require.NotNil(t, identity)
samples := [][3]string{
{"rtmp://example.com/live/stream", "read", "rtmp://example.com/live/stream"},
{"rtmp://example.com/live/stream", "write", "rtmp://example.com/live/stream"},
{"rtmp://localhost:1936/live/stream/token", "read", "rtmp://localhost:1936/live/stream/token"},
{"rtmp://localhost:1936/live/stream?token=token", "write", "rtmp://localhost:1936/live/stream?token=token"},
{"rtmp://localhost:1935/live/stream?token=token", "read", "rtmp://localhost:1935/live/stream?token=" + url.QueryEscape("foobar:servicetoken")},
{"rtmp://localhost:1935/live/stream/token", "write", "rtmp://localhost:1935/live/stream?token=" + url.QueryEscape("foobar:servicetoken")},
}
for _, e := range samples {
rewritten := rewrite.RewriteAddress(e[0], identity, Access(e[1]))
require.Equal(t, e[2], rewritten, "%s %s", e[0], e[1])
}
}
func TestRewriteSRT(t *testing.T) {
im := getIdentityManager(false)
rewrite, err := New(Config{
SRTBase: "srt://localhost:6000/",
})
require.NoError(t, err)
require.NotNil(t, rewrite)
identity, err := im.GetVerifier("foobar")
require.NoError(t, err)
require.NotNil(t, identity)
samples := [][3]string{
{"srt://example.com/?streamid=stream", "read", "srt://example.com/?streamid=stream"},
{"srt://example.com/?streamid=stream", "write", "srt://example.com/?streamid=stream"},
{"srt://localhost:1936/?streamid=live/stream", "read", "srt://localhost:1936/?streamid=live/stream"},
{"srt://localhost:1936/?streamid=live/stream", "write", "srt://localhost:1936/?streamid=live/stream"},
{"srt://localhost:6000/?streamid=live/stream,mode:publish,token:token", "read", "srt://localhost:6000/?streamid=" + url.QueryEscape("live/stream,token:foobar:servicetoken")},
{"srt://localhost:6000/?streamid=live/stream,mode:publish,token:token", "write", "srt://localhost:6000/?streamid=" + url.QueryEscape("live/stream,mode:publish,token:foobar:servicetoken")},
{"srt://localhost:6000/?streamid=" + url.QueryEscape("#!:r=live/stream,m=publish,token=token"), "read", "srt://localhost:6000/?streamid=" + url.QueryEscape("live/stream,token:foobar:servicetoken")},
{"srt://localhost:6000/?streamid=" + url.QueryEscape("#!:r=live/stream,m=publish,token=token"), "write", "srt://localhost:6000/?streamid=" + url.QueryEscape("live/stream,mode:publish,token:foobar:servicetoken")},
}
for _, e := range samples {
rewritten := rewrite.RewriteAddress(e[0], identity, Access(e[1]))
require.Equal(t, e[2], rewritten, "%s %s", e[0], e[1])
}
}

View File

@@ -16,7 +16,7 @@ type StoreData struct {
func NewStoreData() StoreData { func NewStoreData() StoreData {
c := StoreData{ c := StoreData{
Version: 4, Version: version,
} }
c.Process = make(map[string]*app.Process) c.Process = make(map[string]*app.Process)

View File

@@ -26,7 +26,10 @@ type jsonStore struct {
lock sync.RWMutex lock sync.RWMutex
} }
var version uint64 = 4 // version 4 -> 5:
// process groups have been added. the indices for the maps are only the process IDs in version 4.
// version 5 adds the group name as suffix to the process ID with a "~".
var version uint64 = 5
func NewJSON(config JSONConfig) (Store, error) { func NewJSON(config JSONConfig) (Store, error) {
s := &jsonStore{ s := &jsonStore{
@@ -123,12 +126,29 @@ func (s *jsonStore) load(filepath string, version uint64) (StoreData, error) {
return r, json.FormatError(jsondata, err) return r, json.FormatError(jsondata, err)
} }
if db.Version != version { if db.Version == 4 {
return r, fmt.Errorf("unsupported version of the DB file (want: %d, have: %d)", version, db.Version) rold := NewStoreData()
} if err = gojson.Unmarshal(jsondata, &rold); err != nil {
return r, json.FormatError(jsondata, err)
}
if err = gojson.Unmarshal(jsondata, &r); err != nil { for id, p := range rold.Process {
return r, json.FormatError(jsondata, err) r.Process[id+"~"] = p
}
for key, p := range rold.Metadata.System {
r.Metadata.System[key] = p
}
for id, p := range rold.Metadata.Process {
r.Metadata.Process[id+"~"] = p
}
} else if db.Version == version {
if err = gojson.Unmarshal(jsondata, &r); err != nil {
return r, json.FormatError(jsondata, err)
}
} else {
return r, fmt.Errorf("unsupported version of the DB file (want: %d, have: %d)", version, db.Version)
} }
s.logger.WithField("file", filepath).Debug().Log("Read data") s.logger.WithField("file", filepath).Debug().Log("Read data")

View File

@@ -76,11 +76,11 @@ func TestNotExists(t *testing.T) {
func TestStore(t *testing.T) { func TestStore(t *testing.T) {
fs := getFS(t) fs := getFS(t)
fs.Remove("./fixtures/v4_store.json") fs.Remove("./fixtures/v5_store.json")
store, err := NewJSON(JSONConfig{ store, err := NewJSON(JSONConfig{
Filesystem: fs, Filesystem: fs,
Filepath: "./fixtures/v4_store.json", Filepath: "./fixtures/v5_store.json",
}) })
require.NoError(t, err) require.NoError(t, err)
@@ -90,13 +90,14 @@ func TestStore(t *testing.T) {
data.Metadata.System["somedata"] = "foobar" data.Metadata.System["somedata"] = "foobar"
store.Store(data) err = store.Store(data)
require.NoError(t, err)
data2, err := store.Load() data2, err := store.Load()
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, data, data2) require.Equal(t, data, data2)
fs.Remove("./fixtures/v4_store.json") fs.Remove("./fixtures/v5_store.json")
} }
func TestInvalidVersion(t *testing.T) { func TestInvalidVersion(t *testing.T) {

164
rtmp/channel.go Normal file
View File

@@ -0,0 +1,164 @@
package rtmp
import (
"context"
"net"
"net/url"
"sync"
"time"
"github.com/datarhei/core/v16/session"
"github.com/datarhei/joy4/av"
"github.com/datarhei/joy4/av/pubsub"
"github.com/datarhei/joy4/format/rtmp"
)
type client struct {
conn connection
id string
createdAt time.Time
txbytes uint64
rxbytes uint64
collector session.Collector
cancel context.CancelFunc
}
func newClient(conn connection, id string, collector session.Collector) *client {
c := &client{
conn: conn,
id: id,
createdAt: time.Now(),
collector: collector,
}
var ctx context.Context
ctx, c.cancel = context.WithCancel(context.Background())
go c.ticker(ctx)
return c
}
func (c *client) ticker(ctx context.Context) {
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
txbytes := c.conn.TxBytes()
rxbytes := c.conn.RxBytes()
c.collector.Ingress(c.id, int64(rxbytes-c.rxbytes))
c.collector.Egress(c.id, int64(txbytes-c.txbytes))
c.txbytes = txbytes
c.rxbytes = rxbytes
}
}
}
func (c *client) Close() {
c.cancel()
c.conn.Close()
}
// channel represents a stream that is sent to the server
type channel struct {
// The packet queue for the stream
queue *pubsub.Queue
// The metadata of the stream
streams []av.CodecData
// Whether the stream has an audio track
hasAudio bool
// Whether the stream has a video track
hasVideo bool
collector session.Collector
path string
reference string
publisher *client
subscriber map[string]*client
lock sync.RWMutex
isProxy bool
}
func newChannel(conn connection, u *url.URL, reference string, remote net.Addr, streams []av.CodecData, isProxy bool, collector session.Collector) *channel {
ch := &channel{
path: u.Path,
reference: reference,
publisher: newClient(conn, u.Path, collector),
subscriber: make(map[string]*client),
collector: collector,
streams: streams,
queue: pubsub.NewQueue(),
isProxy: isProxy,
}
ch.queue.WriteHeader(streams)
addr := remote.String()
ip, _, _ := net.SplitHostPort(addr)
if collector.IsCollectableIP(ip) {
collector.RegisterAndActivate(ch.path, ch.reference, "publish:"+ch.path, addr)
}
return ch
}
func (ch *channel) Close() {
if ch.publisher == nil {
return
}
ch.publisher.Close()
ch.publisher = nil
ch.queue.Close()
}
func (ch *channel) AddSubscriber(conn *rtmp.Conn) string {
addr := conn.NetConn().RemoteAddr().String()
ip, _, _ := net.SplitHostPort(addr)
client := newClient(conn, addr, ch.collector)
if ch.collector.IsCollectableIP(ip) {
ch.collector.RegisterAndActivate(addr, ch.reference, "play:"+conn.URL.Path, addr)
}
ch.lock.Lock()
ch.subscriber[addr] = client
ch.lock.Unlock()
return addr
}
func (ch *channel) RemoveSubscriber(id string) {
ch.lock.Lock()
defer ch.lock.Unlock()
client := ch.subscriber[id]
if client != nil {
delete(ch.subscriber, id)
client.Close()
}
// If this is a proxied channel and the last subscriber leaves,
// close the channel.
if len(ch.subscriber) == 0 && ch.isProxy {
ch.Close()
}
}

104
rtmp/connection.go Normal file
View File

@@ -0,0 +1,104 @@
package rtmp
import (
"fmt"
"github.com/datarhei/joy4/av"
)
type connection interface {
av.MuxCloser
av.DemuxCloser
TxBytes() uint64
RxBytes() uint64
}
// conn implements the connection interface
type conn struct {
muxer av.MuxCloser
demuxer av.DemuxCloser
txbytes uint64
rxbytes uint64
}
// Make sure that conn implements the connection interface
var _ connection = &conn{}
func newConnectionFromDemuxer(m av.DemuxCloser) connection {
c := &conn{
demuxer: m,
}
return c
}
func (c *conn) TxBytes() uint64 {
return c.txbytes
}
func (c *conn) RxBytes() uint64 {
return c.rxbytes
}
func (c *conn) ReadPacket() (av.Packet, error) {
if c.demuxer != nil {
p, err := c.demuxer.ReadPacket()
if err == nil {
c.rxbytes += uint64(len(p.Data))
}
return p, err
}
return av.Packet{}, fmt.Errorf("no demuxer available")
}
func (c *conn) Streams() ([]av.CodecData, error) {
if c.demuxer != nil {
return c.demuxer.Streams()
}
return nil, fmt.Errorf("no demuxer available")
}
func (c *conn) WritePacket(p av.Packet) error {
if c.muxer != nil {
err := c.muxer.WritePacket(p)
if err == nil {
c.txbytes += uint64(len(p.Data))
}
return err
}
return fmt.Errorf("no muxer available")
}
func (c *conn) WriteHeader(streams []av.CodecData) error {
if c.muxer != nil {
return c.muxer.WriteHeader(streams)
}
return fmt.Errorf("no muxer available")
}
func (c *conn) WriteTrailer() error {
if c.muxer != nil {
return c.muxer.WriteTrailer()
}
return fmt.Errorf("no muxer available")
}
func (c *conn) Close() error {
if c.muxer != nil {
return c.muxer.Close()
}
if c.demuxer != nil {
return c.demuxer.Close()
}
return nil
}

View File

@@ -2,7 +2,6 @@
package rtmp package rtmp
import ( import (
"context"
"crypto/tls" "crypto/tls"
"fmt" "fmt"
"net" "net"
@@ -10,7 +9,6 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"sync" "sync"
"time"
"github.com/datarhei/core/v16/iam" "github.com/datarhei/core/v16/iam"
"github.com/datarhei/core/v16/log" "github.com/datarhei/core/v16/log"
@@ -18,9 +16,7 @@ import (
"github.com/datarhei/joy4/av/avutil" "github.com/datarhei/joy4/av/avutil"
"github.com/datarhei/joy4/av/pktque" "github.com/datarhei/joy4/av/pktque"
"github.com/datarhei/joy4/av/pubsub"
"github.com/datarhei/joy4/format" "github.com/datarhei/joy4/format"
"github.com/datarhei/joy4/format/flv/flvio"
"github.com/datarhei/joy4/format/rtmp" "github.com/datarhei/joy4/format/rtmp"
) )
@@ -32,142 +28,6 @@ func init() {
format.RegisterAll() format.RegisterAll()
} }
type client struct {
conn *rtmp.Conn
id string
createdAt time.Time
txbytes uint64
rxbytes uint64
collector session.Collector
cancel context.CancelFunc
}
func newClient(conn *rtmp.Conn, id string, collector session.Collector) *client {
c := &client{
conn: conn,
id: id,
createdAt: time.Now(),
collector: collector,
}
var ctx context.Context
ctx, c.cancel = context.WithCancel(context.Background())
go c.ticker(ctx)
return c
}
func (c *client) ticker(ctx context.Context) {
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
txbytes := c.conn.TxBytes()
rxbytes := c.conn.RxBytes()
c.collector.Ingress(c.id, int64(rxbytes-c.rxbytes))
c.collector.Egress(c.id, int64(txbytes-c.txbytes))
c.txbytes = txbytes
c.rxbytes = rxbytes
}
}
}
func (c *client) Close() {
c.cancel()
}
// channel represents a stream that is sent to the server
type channel struct {
// The packet queue for the stream
queue *pubsub.Queue
// The metadata of the stream
metadata flvio.AMFMap
// Whether the stream has an audio track
hasAudio bool
// Whether the stream has a video track
hasVideo bool
collector session.Collector
path string
reference string
publisher *client
subscriber map[string]*client
lock sync.RWMutex
}
func newChannel(conn *rtmp.Conn, reference string, collector session.Collector) *channel {
ch := &channel{
path: conn.URL.Path,
reference: reference,
publisher: newClient(conn, conn.URL.Path, collector),
subscriber: make(map[string]*client),
collector: collector,
}
addr := conn.NetConn().RemoteAddr().String()
ip, _, _ := net.SplitHostPort(addr)
if collector.IsCollectableIP(ip) {
collector.RegisterAndActivate(ch.path, ch.reference, "publish:"+ch.path, addr)
}
return ch
}
func (ch *channel) Close() {
if ch.publisher == nil {
return
}
ch.publisher.Close()
ch.publisher = nil
ch.queue.Close()
}
func (ch *channel) AddSubscriber(conn *rtmp.Conn) string {
addr := conn.NetConn().RemoteAddr().String()
ip, _, _ := net.SplitHostPort(addr)
client := newClient(conn, addr, ch.collector)
if ch.collector.IsCollectableIP(ip) {
ch.collector.RegisterAndActivate(addr, ch.reference, "play:"+ch.path, addr)
}
ch.lock.Lock()
ch.subscriber[addr] = client
ch.lock.Unlock()
return addr
}
func (ch *channel) RemoveSubscriber(id string) {
ch.lock.Lock()
defer ch.lock.Unlock()
client := ch.subscriber[id]
if client != nil {
delete(ch.subscriber, id)
client.Close()
}
}
// Config for a new RTMP server // Config for a new RTMP server
type Config struct { type Config struct {
// Logger. Optional. // Logger. Optional.
@@ -333,17 +193,15 @@ func (s *server) log(who, action, path, message string, client net.Addr) {
}).Log(message) }).Log(message)
} }
// getToken returns the path and the token found in the URL. If the token // GetToken returns the path and the token found in the URL. If the token
// was part of the path, the token is removed from the path. The token in // was part of the path, the token is removed from the path. The token in
// the query string takes precedence. The token in the path is assumed to // the query string takes precedence. The token in the path is assumed to
// be the last path element. // be the last path element.
func getToken(u *url.URL) (string, string) { func GetToken(u *url.URL) (string, string) {
q := u.Query() q := u.Query()
token := q.Get("token") if q.Has("token") {
if len(token) != 0 {
// The token was in the query. Return the unmomdified path and the token // The token was in the query. Return the unmomdified path and the token
return u.Path, token return u.Path, q.Get("token")
} }
pathElements := strings.Split(u.EscapedPath(), "/") pathElements := strings.Split(u.EscapedPath(), "/")
@@ -359,35 +217,24 @@ func getToken(u *url.URL) (string, string) {
// handlePlay is called when a RTMP client wants to play a stream // handlePlay is called when a RTMP client wants to play a stream
func (s *server) handlePlay(conn *rtmp.Conn) { func (s *server) handlePlay(conn *rtmp.Conn) {
client := conn.NetConn().RemoteAddr()
defer conn.Close() defer conn.Close()
playPath, token := getToken(conn.URL) remote := conn.NetConn().RemoteAddr()
playPath, token := GetToken(conn.URL)
identity, err := s.findIdentityFromStreamKey(token) identity, err := s.findIdentityFromStreamKey(token)
if err != nil { if err != nil {
s.logger.Debug().WithError(err).Log("no valid identity found") s.logger.Debug().WithError(err).Log("invalid streamkey")
s.log("PLAY", "FORBIDDEN", playPath, "invalid streamkey ("+token+")", client) s.log("PLAY", "FORBIDDEN", playPath, "invalid streamkey ("+token+")", remote)
return return
} }
domain := s.findDomainFromPlaypath(playPath) domain := s.findDomainFromPlaypath(playPath)
resource := "rtmp:" + playPath resource := "rtmp:" + playPath
l := s.logger.Debug().WithFields(log.Fields{ if !s.iam.Enforce(identity, domain, resource, "PLAY") {
"name": identity.Name(), s.log("PLAY", "FORBIDDEN", playPath, "access denied", remote)
"domain": domain,
"resource": resource,
"action": "PLAY",
})
if ok, rule := s.iam.Enforce(identity.Name(), domain, resource, "PLAY"); !ok {
l.Log("access denied")
s.log("PLAY", "FORBIDDEN", playPath, "invalid streamkey ("+token+")", client)
return return
} else {
l.Log(rule)
} }
/* /*
@@ -415,10 +262,10 @@ func (s *server) handlePlay(conn *rtmp.Conn) {
s.lock.RUnlock() s.lock.RUnlock()
if ch != nil { if ch != nil {
// Set the metadata for the client // Send the metadata to the client
conn.SetMetaData(ch.metadata) conn.WriteHeader(ch.streams)
s.log("PLAY", "START", playPath, "", client) s.log("PLAY", "START", conn.URL.Path, "", remote)
// Get a cursor and apply filters // Get a cursor and apply filters
cursor := ch.queue.Oldest() cursor := ch.queue.Oldest()
@@ -440,75 +287,68 @@ func (s *server) handlePlay(conn *rtmp.Conn) {
id := ch.AddSubscriber(conn) id := ch.AddSubscriber(conn)
// Transfer the data // Transfer the data, blocks until done
avutil.CopyFile(conn, demuxer) avutil.CopyFile(conn, demuxer)
ch.RemoveSubscriber(id) ch.RemoveSubscriber(id)
s.log("PLAY", "STOP", playPath, "", client) s.log("PLAY", "STOP", playPath, "", remote)
} else { } else {
s.log("PLAY", "NOTFOUND", playPath, "", client) s.log("PLAY", "NOTFOUND", playPath, "", remote)
} }
} }
// handlePublish is called when a RTMP client wants to publish a stream // handlePublish is called when a RTMP client wants to publish a stream
func (s *server) handlePublish(conn *rtmp.Conn) { func (s *server) handlePublish(conn *rtmp.Conn) {
client := conn.NetConn().RemoteAddr()
defer conn.Close() defer conn.Close()
playPath, token := getToken(conn.URL) remote := conn.NetConn().RemoteAddr()
playPath, token := GetToken(conn.URL)
// Check the app patch // Check the app patch
if !strings.HasPrefix(playPath, s.app) { if !strings.HasPrefix(playPath, s.app) {
s.log("PUBLISH", "FORBIDDEN", conn.URL.Path, "invalid app", client) s.log("PUBLISH", "FORBIDDEN", conn.URL.Path, "invalid app", remote)
return return
} }
identity, err := s.findIdentityFromStreamKey(token) identity, err := s.findIdentityFromStreamKey(token)
if err != nil { if err != nil {
s.logger.Debug().WithError(err).Log("no valid identity found") s.logger.Debug().WithError(err).Log("invalid streamkey")
s.log("PUBLISH", "FORBIDDEN", playPath, "invalid streamkey ("+token+")", client) s.log("PUBLISH", "FORBIDDEN", playPath, "invalid streamkey ("+token+")", remote)
return return
} }
domain := s.findDomainFromPlaypath(playPath) domain := s.findDomainFromPlaypath(playPath)
resource := "rtmp:" + playPath resource := "rtmp:" + playPath
l := s.logger.Debug().WithFields(log.Fields{ if !s.iam.Enforce(identity, domain, resource, "PUBLISH") {
"name": identity.Name(), s.log("PUBLISH", "FORBIDDEN", playPath, "access denied", remote)
"domain": domain,
"resource": resource,
"action": "PUBLISH",
})
if ok, rule := s.iam.Enforce(identity.Name(), domain, "rtmp:"+playPath, "PUBLISH"); !ok {
l.Log("access denied")
s.log("PUBLISH", "FORBIDDEN", playPath, "invalid streamkey ("+token+")", client)
return return
} else {
l.Log(rule)
} }
// Check the stream if it contains any valid/known streams err = s.publish(conn, conn.URL, remote, false)
streams, _ := conn.Streams() if err != nil {
s.logger.WithField("path", conn.URL.Path).WithError(err).Log("")
}
}
func (s *server) publish(src connection, u *url.URL, remote net.Addr, isProxy bool) error {
// Check the streams if it contains any valid/known streams
streams, _ := src.Streams()
if len(streams) == 0 { if len(streams) == 0 {
s.log("PUBLISH", "INVALID", playPath, "no streams available", client) s.log("PUBLISH", "INVALID", u.Path, "no streams available", remote)
return return fmt.Errorf("no streams are available")
} }
s.lock.Lock() s.lock.Lock()
ch := s.channels[conn.URL.Path] ch := s.channels[u.Path]
if ch == nil { if ch == nil {
reference := strings.TrimPrefix(strings.TrimSuffix(playPath, filepath.Ext(playPath)), s.app+"/") reference := strings.TrimPrefix(strings.TrimSuffix(u.Path, filepath.Ext(u.Path)), s.app+"/")
// Create a new channel // Create a new channel
ch = newChannel(conn, reference, s.collector) ch = newChannel(src, u, reference, remote, streams, isProxy, s.collector)
ch.metadata = conn.GetMetaData()
ch.queue = pubsub.NewQueue()
ch.queue.WriteHeader(streams)
for _, stream := range streams { for _, stream := range streams {
typ := stream.Type() typ := stream.Type()
@@ -521,7 +361,7 @@ func (s *server) handlePublish(conn *rtmp.Conn) {
} }
} }
s.channels[playPath] = ch s.channels[u.Path] = ch
} else { } else {
ch = nil ch = nil
} }
@@ -529,48 +369,58 @@ func (s *server) handlePublish(conn *rtmp.Conn) {
s.lock.Unlock() s.lock.Unlock()
if ch == nil { if ch == nil {
s.log("PUBLISH", "CONFLICT", playPath, "already publishing", client) s.log("PUBLISH", "CONFLICT", u.Path, "already publishing", remote)
return return fmt.Errorf("already publishing")
} }
s.log("PUBLISH", "START", playPath, "", client) s.log("PUBLISH", "START", u.Path, "", remote)
for _, stream := range streams { for _, stream := range streams {
s.log("PUBLISH", "STREAM", playPath, stream.Type().String(), client) s.log("PUBLISH", "STREAM", u.Path, stream.Type().String(), remote)
} }
// Ingest the data // Ingest the data, blocks until done
avutil.CopyPackets(ch.queue, conn) avutil.CopyPackets(ch.queue, src)
s.lock.Lock() s.lock.Lock()
delete(s.channels, playPath) delete(s.channels, u.Path)
s.lock.Unlock() s.lock.Unlock()
ch.Close() ch.Close()
s.log("PUBLISH", "STOP", playPath, "", client) s.log("PUBLISH", "STOP", u.Path, "", remote)
return nil
} }
func (s *server) findIdentityFromStreamKey(key string) (iam.IdentityVerifier, error) { func (s *server) findIdentityFromStreamKey(key string) (string, error) {
if len(key) == 0 {
return "$anon", nil
}
var identity iam.IdentityVerifier var identity iam.IdentityVerifier
var err error var err error
var token string
elements := strings.Split(key, ":") elements := strings.Split(key, ":")
if len(elements) == 1 { if len(elements) == 1 {
identity, err = s.iam.GetDefaultIdentity() identity, err = s.iam.GetDefaultIdentity()
token = elements[0]
} else { } else {
identity, err = s.iam.GetIdentity(elements[0]) identity, err = s.iam.GetIdentity(elements[0])
token = elements[1]
} }
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid token: %w", err) return "$anon", nil
} }
if ok, err := identity.VerifyServiceToken(elements[1]); !ok { if ok, err := identity.VerifyServiceToken(token); !ok {
return nil, fmt.Errorf("invalid token: %w", err) return "$anon", fmt.Errorf("invalid token: %w", err)
} }
return identity, nil return identity.Name(), nil
} }
func (s *server) findDomainFromPlaypath(path string) string { func (s *server) findDomainFromPlaypath(path string) string {
@@ -578,7 +428,7 @@ func (s *server) findDomainFromPlaypath(path string) string {
elements := strings.Split(path, "/") elements := strings.Split(path, "/")
if len(elements) == 1 { if len(elements) == 1 {
return "" return "$none"
} }
domain := elements[0] domain := elements[0]
@@ -587,5 +437,5 @@ func (s *server) findDomainFromPlaypath(path string) string {
return domain return domain
} }
return "" return "$none"
} }

View File

@@ -18,7 +18,7 @@ func TestToken(t *testing.T) {
u, err := url.Parse(d[0]) u, err := url.Parse(d[0])
require.NoError(t, err) require.NoError(t, err)
path, token := getToken(u) path, token := GetToken(u)
require.Equal(t, d[1], path, "url=%s", u.String()) require.Equal(t, d[1], path, "url=%s", u.String())
require.Equal(t, d[2], token, "url=%s", u.String()) require.Equal(t, d[2], token, "url=%s", u.String())

147
srt/channel.go Normal file
View File

@@ -0,0 +1,147 @@
package srt
import (
"context"
"net"
"sync"
"time"
"github.com/datarhei/core/v16/session"
srt "github.com/datarhei/gosrt"
)
type client struct {
conn srt.Conn
id string
createdAt time.Time
txbytes uint64
rxbytes uint64
collector session.Collector
cancel context.CancelFunc
}
func newClient(conn srt.Conn, id string, collector session.Collector) *client {
c := &client{
conn: conn,
id: id,
createdAt: time.Now(),
collector: collector,
}
var ctx context.Context
ctx, c.cancel = context.WithCancel(context.Background())
go c.ticker(ctx)
return c
}
func (c *client) ticker(ctx context.Context) {
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
stats := &srt.Statistics{}
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
c.conn.Stats(stats)
rxbytes := stats.Accumulated.ByteRecv
txbytes := stats.Accumulated.ByteSent
c.collector.Ingress(c.id, int64(rxbytes-c.rxbytes))
c.collector.Egress(c.id, int64(txbytes-c.txbytes))
c.txbytes = txbytes
c.rxbytes = rxbytes
}
}
}
func (c *client) Close() {
c.cancel()
c.conn.Close()
}
// channel represents a stream that is sent to the server
type channel struct {
pubsub srt.PubSub
collector session.Collector
path string
publisher *client
subscriber map[string]*client
lock sync.RWMutex
isProxy bool
}
func newChannel(conn srt.Conn, resource string, isProxy bool, collector session.Collector) *channel {
ch := &channel{
pubsub: srt.NewPubSub(srt.PubSubConfig{}),
path: resource,
publisher: newClient(conn, resource, collector),
subscriber: make(map[string]*client),
collector: collector,
isProxy: isProxy,
}
addr := conn.RemoteAddr().String()
ip, _, _ := net.SplitHostPort(addr)
if collector.IsCollectableIP(ip) {
collector.RegisterAndActivate(resource, resource, "publish:"+resource, addr)
}
return ch
}
func (ch *channel) Close() {
if ch.publisher == nil {
return
}
ch.publisher.Close()
ch.publisher = nil
}
func (ch *channel) AddSubscriber(conn srt.Conn, resource string) string {
addr := conn.RemoteAddr().String()
ip, _, _ := net.SplitHostPort(addr)
client := newClient(conn, addr, ch.collector)
if ch.collector.IsCollectableIP(ip) {
ch.collector.RegisterAndActivate(addr, resource, "play:"+resource, addr)
}
ch.lock.Lock()
ch.subscriber[addr] = client
ch.lock.Unlock()
return addr
}
func (ch *channel) RemoveSubscriber(id string) {
ch.lock.Lock()
defer ch.lock.Unlock()
client := ch.subscriber[id]
if client != nil {
delete(ch.subscriber, id)
client.Close()
}
// If this is a proxied channel and the last subscriber leaves,
// close the channel.
if len(ch.subscriber) == 0 && ch.isProxy {
ch.Close()
}
}

View File

@@ -5,7 +5,6 @@ import (
"context" "context"
"fmt" "fmt"
"net" "net"
"regexp"
"strings" "strings"
"sync" "sync"
"time" "time"
@@ -13,6 +12,7 @@ import (
"github.com/datarhei/core/v16/iam" "github.com/datarhei/core/v16/iam"
"github.com/datarhei/core/v16/log" "github.com/datarhei/core/v16/log"
"github.com/datarhei/core/v16/session" "github.com/datarhei/core/v16/session"
"github.com/datarhei/core/v16/srt/url"
srt "github.com/datarhei/gosrt" srt "github.com/datarhei/gosrt"
) )
@@ -20,132 +20,6 @@ import (
// has been closed regularly with the Close() function. // has been closed regularly with the Close() function.
var ErrServerClosed = srt.ErrServerClosed var ErrServerClosed = srt.ErrServerClosed
type client struct {
conn srt.Conn
id string
createdAt time.Time
txbytes uint64
rxbytes uint64
collector session.Collector
cancel context.CancelFunc
}
func newClient(conn srt.Conn, id string, collector session.Collector) *client {
c := &client{
conn: conn,
id: id,
createdAt: time.Now(),
collector: collector,
}
var ctx context.Context
ctx, c.cancel = context.WithCancel(context.Background())
go c.ticker(ctx)
return c
}
func (c *client) ticker(ctx context.Context) {
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
stats := &srt.Statistics{}
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
c.conn.Stats(stats)
rxbytes := stats.Accumulated.ByteRecv
txbytes := stats.Accumulated.ByteSent
c.collector.Ingress(c.id, int64(rxbytes-c.rxbytes))
c.collector.Egress(c.id, int64(txbytes-c.txbytes))
c.txbytes = txbytes
c.rxbytes = rxbytes
}
}
}
func (c *client) Close() {
c.cancel()
}
// channel represents a stream that is sent to the server
type channel struct {
pubsub srt.PubSub
collector session.Collector
path string
publisher *client
subscriber map[string]*client
lock sync.RWMutex
}
func newChannel(conn srt.Conn, resource string, collector session.Collector) *channel {
ch := &channel{
pubsub: srt.NewPubSub(srt.PubSubConfig{}),
path: resource,
publisher: newClient(conn, resource, collector),
subscriber: make(map[string]*client),
collector: collector,
}
addr := conn.RemoteAddr().String()
ip, _, _ := net.SplitHostPort(addr)
if collector.IsCollectableIP(ip) {
collector.RegisterAndActivate(resource, resource, "publish:"+resource, addr)
}
return ch
}
func (ch *channel) Close() {
if ch.publisher == nil {
return
}
ch.publisher.Close()
ch.publisher = nil
}
func (ch *channel) AddSubscriber(conn srt.Conn, resource string) string {
addr := conn.RemoteAddr().String()
ip, _, _ := net.SplitHostPort(addr)
client := newClient(conn, addr, ch.collector)
if ch.collector.IsCollectableIP(ip) {
ch.collector.RegisterAndActivate(addr, resource, "play:"+resource, addr)
}
ch.lock.Lock()
ch.subscriber[addr] = client
ch.lock.Unlock()
return addr
}
func (ch *channel) RemoveSubscriber(id string) {
ch.lock.Lock()
defer ch.lock.Unlock()
client := ch.subscriber[id]
if client != nil {
delete(ch.subscriber, id)
client.Close()
}
}
// Config for a new SRT server // Config for a new SRT server
type Config struct { type Config struct {
// The address the SRT server should listen on, e.g. ":1935" // The address the SRT server should listen on, e.g. ":1935"
@@ -369,165 +243,64 @@ func (s *server) log(handler, action, resource, message string, client net.Addr)
}).Log(message) }).Log(message)
} }
type streamInfo struct {
mode string
resource string
token string
}
func parseStreamId(streamid string) (streamInfo, error) {
si := streamInfo{}
if strings.HasPrefix(streamid, "#!:") {
return parseOldStreamId(streamid)
}
re := regexp.MustCompile(`,(token|mode):(.+)`)
results := map[string]string{}
idEnd := -1
value := streamid
key := ""
for {
matches := re.FindStringSubmatchIndex(value)
if matches == nil {
break
}
if idEnd < 0 {
idEnd = matches[2] - 1
}
if len(key) != 0 {
results[key] = value[:matches[2]-1]
}
key = value[matches[2]:matches[3]]
value = value[matches[4]:matches[5]]
results[key] = value
}
if idEnd < 0 {
idEnd = len(streamid)
}
si.resource = streamid[:idEnd]
if token, ok := results["token"]; ok {
si.token = token
}
if mode, ok := results["mode"]; ok {
si.mode = mode
} else {
si.mode = "request"
}
return si, nil
}
func parseOldStreamId(streamid string) (streamInfo, error) {
si := streamInfo{}
if !strings.HasPrefix(streamid, "#!:") {
return si, fmt.Errorf("unknown streamid format")
}
streamid = strings.TrimPrefix(streamid, "#!:")
kvs := strings.Split(streamid, ",")
splitFn := func(s, sep string) (string, string, error) {
splitted := strings.SplitN(s, sep, 2)
if len(splitted) != 2 {
return "", "", fmt.Errorf("invalid key/value pair")
}
return splitted[0], splitted[1], nil
}
for _, kv := range kvs {
key, value, err := splitFn(kv, "=")
if err != nil {
continue
}
switch key {
case "m":
si.mode = value
case "r":
si.resource = value
case "token":
si.token = value
default:
}
}
return si, nil
}
func (s *server) handleConnect(req srt.ConnRequest) srt.ConnType { func (s *server) handleConnect(req srt.ConnRequest) srt.ConnType {
mode := srt.REJECT mode := srt.REJECT
client := req.RemoteAddr() client := req.RemoteAddr()
streamId := req.StreamId() streamId := req.StreamId()
si, err := parseStreamId(streamId) si, err := url.ParseStreamId(streamId)
if err != nil { if err != nil {
s.log("CONNECT", "INVALID", "", err.Error(), client) s.log("CONNECT", "INVALID", "", err.Error(), client)
return srt.REJECT return srt.REJECT
} }
if len(si.resource) == 0 { if len(si.Resource) == 0 {
s.log("CONNECT", "INVALID", "", "stream resource not provided", client) s.log("CONNECT", "INVALID", "", "stream resource not provided", client)
return srt.REJECT return srt.REJECT
} }
if si.mode == "publish" { if si.Mode == "publish" {
mode = srt.PUBLISH mode = srt.PUBLISH
} else if si.mode == "request" { } else if si.Mode == "request" {
mode = srt.SUBSCRIBE mode = srt.SUBSCRIBE
} else { } else {
s.log("CONNECT", "INVALID", si.resource, "invalid connection mode", client) s.log("CONNECT", "INVALID", si.Resource, "invalid connection mode", client)
return srt.REJECT return srt.REJECT
} }
if len(s.passphrase) != 0 { if len(s.passphrase) != 0 {
if !req.IsEncrypted() { if !req.IsEncrypted() {
s.log("CONNECT", "FORBIDDEN", si.resource, "connection has to be encrypted", client) s.log("CONNECT", "FORBIDDEN", si.Resource, "connection has to be encrypted", client)
return srt.REJECT return srt.REJECT
} }
if err := req.SetPassphrase(s.passphrase); err != nil { if err := req.SetPassphrase(s.passphrase); err != nil {
s.log("CONNECT", "FORBIDDEN", si.resource, err.Error(), client) s.log("CONNECT", "FORBIDDEN", si.Resource, err.Error(), client)
return srt.REJECT return srt.REJECT
} }
} else { } else {
if req.IsEncrypted() { if req.IsEncrypted() {
s.log("CONNECT", "INVALID", si.resource, "connection must not be encrypted", client) s.log("CONNECT", "INVALID", si.Resource, "connection must not be encrypted", client)
return srt.REJECT return srt.REJECT
} }
} }
// Check the token identity, err := s.findIdentityFromToken(si.Token)
if len(s.token) != 0 && s.token != si.token { if err != nil {
s.log("CONNECT", "FORBIDDEN", si.resource, "invalid token ("+si.token+")", client) s.logger.Debug().WithError(err).Log("invalid token")
s.log("PUBLISH", "FORBIDDEN", si.Resource, "invalid token", client)
return srt.REJECT return srt.REJECT
} }
s.lock.RLock() domain := s.findDomainFromPlaypath(si.Resource)
ch := s.channels[si.resource] resource := "srt:" + si.Resource
s.lock.RUnlock() action := "PLAY"
if mode == srt.PUBLISH {
if mode == srt.PUBLISH && ch != nil { action = "PUBLISH"
s.log("CONNECT", "CONFLICT", si.resource, "already publishing", client)
return srt.REJECT
} }
if mode == srt.SUBSCRIBE && ch == nil { if !s.iam.Enforce(identity, domain, resource, action) {
s.log("CONNECT", "NOTFOUND", si.resource, "no publisher for this resource found", client) s.log("PUBLISH", "FORBIDDEN", si.Resource, "access denied", client)
return srt.REJECT return srt.REJECT
} }
@@ -538,61 +311,36 @@ func (s *server) handlePublish(conn srt.Conn) {
streamId := conn.StreamId() streamId := conn.StreamId()
client := conn.RemoteAddr() client := conn.RemoteAddr()
si, _ := parseStreamId(streamId) si, _ := url.ParseStreamId(streamId)
identity, err := s.findIdentityFromToken(si.token)
if err != nil {
s.logger.Debug().WithError(err).Log("no valid identity found")
s.log("PUBLISH", "FORBIDDEN", si.resource, "invalid token", client)
return
}
domain := s.findDomainFromPlaypath(si.resource)
resource := "srt:" + si.resource
l := s.logger.Debug().WithFields(log.Fields{
"name": identity.Name(),
"domain": domain,
"resource": resource,
"action": "PUBLISH",
})
if ok, rule := s.iam.Enforce(identity.Name(), domain, resource, "PUBLISH"); !ok {
l.Log("access denied")
s.log("PUBLISH", "FORBIDDEN", si.resource, "invalid token", client)
return
} else {
l.Log(rule)
}
// Look for the stream // Look for the stream
s.lock.Lock() s.lock.Lock()
ch := s.channels[si.resource] ch := s.channels[si.Resource]
if ch == nil { if ch == nil {
ch = newChannel(conn, si.resource, s.collector) ch = newChannel(conn, si.Resource, false, s.collector)
s.channels[si.resource] = ch s.channels[si.Resource] = ch
} else { } else {
ch = nil ch = nil
} }
s.lock.Unlock() s.lock.Unlock()
if ch == nil { if ch == nil {
s.log("PUBLISH", "CONFLICT", si.resource, "already publishing", client) s.log("PUBLISH", "CONFLICT", si.Resource, "already publishing", client)
conn.Close() conn.Close()
return return
} }
s.log("PUBLISH", "START", si.resource, "", client) s.log("PUBLISH", "START", si.Resource, "", client)
ch.pubsub.Publish(conn) ch.pubsub.Publish(conn)
s.lock.Lock() s.lock.Lock()
delete(s.channels, si.resource) delete(s.channels, si.Resource)
s.lock.Unlock() s.lock.Unlock()
ch.Close() ch.Close()
s.log("PUBLISH", "STOP", si.resource, "", client) s.log("PUBLISH", "STOP", si.Resource, "", client)
conn.Close() conn.Close()
} }
@@ -601,83 +349,66 @@ func (s *server) handleSubscribe(conn srt.Conn) {
streamId := conn.StreamId() streamId := conn.StreamId()
client := conn.RemoteAddr() client := conn.RemoteAddr()
si, _ := parseStreamId(streamId) si, _ := url.ParseStreamId(streamId)
identity, err := s.findIdentityFromToken(si.token)
if err != nil {
s.logger.Debug().WithError(err).Log("no valid identity found")
s.log("SUBSCRIBE", "FORBIDDEN", si.resource, "invalid token", client)
return
}
domain := s.findDomainFromPlaypath(si.resource)
resource := "srt:" + si.resource
l := s.logger.Debug().WithFields(log.Fields{
"name": identity.Name(),
"domain": domain,
"resource": resource,
"action": "PLAY",
})
if ok, rule := s.iam.Enforce(identity.Name(), domain, resource, "PLAY"); !ok {
l.Log("access denied")
s.log("SUBSCRIBE", "FORBIDDEN", si.resource, "invalid token", client)
return
} else {
l.Log(rule)
}
// Look for the stream // Look for the stream
s.lock.RLock() s.lock.RLock()
ch := s.channels[si.resource] ch := s.channels[si.Resource]
s.lock.RUnlock() s.lock.RUnlock()
if ch == nil { if ch == nil {
s.log("SUBSCRIBE", "NOTFOUND", si.resource, "no publisher for this resource found", client) s.log("SUBSCRIBE", "NOTFOUND", si.Resource, "no publisher for this resource found", client)
conn.Close() conn.Close()
return return
} }
s.log("SUBSCRIBE", "START", si.resource, "", client) s.log("SUBSCRIBE", "START", si.Resource, "", client)
id := ch.AddSubscriber(conn, si.resource) id := ch.AddSubscriber(conn, si.Resource)
ch.pubsub.Subscribe(conn) ch.pubsub.Subscribe(conn)
s.log("SUBSCRIBE", "STOP", si.resource, "", client) s.log("SUBSCRIBE", "STOP", si.Resource, "", client)
ch.RemoveSubscriber(id) ch.RemoveSubscriber(id)
conn.Close() conn.Close()
} }
func (s *server) findIdentityFromToken(key string) (iam.IdentityVerifier, error) { func (s *server) findIdentityFromToken(key string) (string, error) {
if len(key) == 0 {
return "$anon", nil
}
var identity iam.IdentityVerifier var identity iam.IdentityVerifier
var err error var err error
var token string
elements := strings.Split(key, ":") elements := strings.Split(key, ":")
if len(elements) == 1 { if len(elements) == 1 {
identity, err = s.iam.GetDefaultIdentity() identity, err = s.iam.GetDefaultIdentity()
token = elements[0]
} else { } else {
identity, err = s.iam.GetIdentity(elements[0]) identity, err = s.iam.GetIdentity(elements[0])
token = elements[1]
} }
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid token: %w", err) return "$anon", nil
} }
if ok, err := identity.VerifyServiceToken(elements[1]); !ok { if ok, err := identity.VerifyServiceToken(token); !ok {
return nil, fmt.Errorf("invalid token: %w", err) return "$anon", fmt.Errorf("invalid token: %w", err)
} }
return identity, nil return identity.Name(), nil
} }
func (s *server) findDomainFromPlaypath(path string) string { func (s *server) findDomainFromPlaypath(path string) string {
elements := strings.Split(path, "/") elements := strings.Split(path, "/")
if len(elements) == 1 { if len(elements) == 1 {
return "" return "$none"
} }
domain := elements[0] domain := elements[0]
@@ -686,5 +417,5 @@ func (s *server) findDomainFromPlaypath(path string) string {
return domain return domain
} }
return "" return "$none"
} }

View File

@@ -1,44 +0,0 @@
package srt
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestParseStreamId(t *testing.T) {
streamids := map[string]streamInfo{
"bla": {resource: "bla", mode: "request"},
"bla,mode:publish": {resource: "bla", mode: "publish"},
"123456789": {resource: "123456789", mode: "request"},
"bla,token:foobar": {resource: "bla", token: "foobar", mode: "request"},
"bla,token:foo,bar": {resource: "bla", token: "foo,bar", mode: "request"},
"123456789,mode:publish,token:foobar": {resource: "123456789", token: "foobar", mode: "publish"},
"mode:publish": {resource: "mode:publish", mode: "request"},
}
for streamid, wantsi := range streamids {
si, err := parseStreamId(streamid)
require.NoError(t, err)
require.Equal(t, wantsi, si)
}
}
func TestParseOldStreamId(t *testing.T) {
streamids := map[string]streamInfo{
"#!:": {},
"#!:key=value": {},
"#!:m=publish": {mode: "publish"},
"#!:r=123456789": {resource: "123456789"},
"#!:token=foobar": {token: "foobar"},
"#!:token=foo,bar": {token: "foo"},
"#!:m=publish,r=123456789,token=foobar": {mode: "publish", resource: "123456789", token: "foobar"},
}
for streamid, wantsi := range streamids {
si, _ := parseOldStreamId(streamid)
require.Equal(t, wantsi, si)
}
}

206
srt/url/url.go Normal file
View File

@@ -0,0 +1,206 @@
package url
import (
"fmt"
neturl "net/url"
"regexp"
"strings"
)
type URL struct {
Scheme string
Host string
StreamId string
Options neturl.Values
}
func Parse(srturl string) (*URL, error) {
u, err := neturl.Parse(srturl)
if err != nil {
return nil, err
}
if u.Scheme != "srt" {
return nil, fmt.Errorf("invalid SRT url")
}
options := u.Query()
streamid := options.Get("streamid")
options.Del("streamid")
su := &URL{
Scheme: "srt",
Host: u.Host,
StreamId: streamid,
Options: options,
}
return su, nil
}
func (su *URL) String() string {
options, _ := neturl.ParseQuery(su.Options.Encode())
options.Set("streamid", su.StreamId)
u := neturl.URL{
Scheme: su.Scheme,
Host: su.Host,
RawQuery: options.Encode(),
}
return u.String()
}
func (su *URL) StreamInfo() (*StreamInfo, error) {
s, err := ParseStreamId(su.StreamId)
if err != nil {
return nil, err
}
return &s, nil
}
func (su *URL) SetStreamInfo(si *StreamInfo) {
su.StreamId = si.String()
}
func (su *URL) Hostname() string {
u := neturl.URL{
Host: su.Host,
}
return u.Hostname()
}
func (su *URL) Port() string {
u := neturl.URL{
Host: su.Host,
}
return u.Port()
}
type StreamInfo struct {
Mode string
Resource string
Token string
}
func (si *StreamInfo) String() string {
streamid := si.Resource
if si.Mode != "request" {
streamid += ",mode:" + si.Mode
}
if len(si.Token) != 0 {
streamid += ",token:" + si.Token
}
return streamid
}
// ParseStreamId parses a streamid. If the streamid is in the old format
// it is detected and parsed accordingly. Otherwith the new simplified
// format will be assumed.
//
// resource[,token:{token}]?[,mode:(publish|*request)]?
//
// If the mode is not provided, "request" will be assumed.
func ParseStreamId(streamid string) (StreamInfo, error) {
si := StreamInfo{}
if strings.HasPrefix(streamid, "#!:") {
return ParseDeprecatedStreamId(streamid)
}
re := regexp.MustCompile(`,(token|mode):(.+)`)
results := map[string]string{}
idEnd := -1
value := streamid
key := ""
for {
matches := re.FindStringSubmatchIndex(value)
if matches == nil {
break
}
if idEnd < 0 {
idEnd = matches[2] - 1
}
if len(key) != 0 {
results[key] = value[:matches[2]-1]
}
key = value[matches[2]:matches[3]]
value = value[matches[4]:matches[5]]
results[key] = value
}
if idEnd < 0 {
idEnd = len(streamid)
}
si.Resource = streamid[:idEnd]
if token, ok := results["token"]; ok {
si.Token = token
}
if mode, ok := results["mode"]; ok {
si.Mode = mode
} else {
si.Mode = "request"
}
return si, nil
}
// ParseDeprecatedStreamId parses a streamid in the old format. The old format
// is based on the recommendation of the SRT specs, but with the special
// character it contains it can cause some trouble in clients (e.g. kiloview
// doesn't like the = character).
func ParseDeprecatedStreamId(streamid string) (StreamInfo, error) {
si := StreamInfo{Mode: "request"}
if !strings.HasPrefix(streamid, "#!:") {
return si, fmt.Errorf("unknown streamid format")
}
streamid = strings.TrimPrefix(streamid, "#!:")
kvs := strings.Split(streamid, ",")
split := func(s, sep string) (string, string, error) {
splitted := strings.SplitN(s, sep, 2)
if len(splitted) != 2 {
return "", "", fmt.Errorf("invalid key/value pair")
}
return splitted[0], splitted[1], nil
}
for _, kv := range kvs {
key, value, err := split(kv, "=")
if err != nil {
continue
}
switch key {
case "m":
si.Mode = value
case "r":
si.Resource = value
case "token":
si.Token = value
default:
}
}
return si, nil
}

67
srt/url/url_test.go Normal file
View File

@@ -0,0 +1,67 @@
package url
import (
"net/url"
"testing"
"github.com/stretchr/testify/require"
)
func TestParse(t *testing.T) {
srturl := "srt://127.0.0.1:6000?mode=caller&passphrase=foobar&streamid=" + url.QueryEscape("#!:m=publish,r=123456,token=bla")
u, err := Parse(srturl)
require.NoError(t, err)
require.Equal(t, "srt", u.Scheme)
require.Equal(t, "127.0.0.1:6000", u.Host)
require.Equal(t, "#!:m=publish,r=123456,token=bla", u.StreamId)
si, err := u.StreamInfo()
require.NoError(t, err)
require.Equal(t, "publish", si.Mode)
require.Equal(t, "123456", si.Resource)
require.Equal(t, "bla", si.Token)
require.Equal(t, srturl, u.String())
srturl = "srt://127.0.0.1:6000?mode=caller&passphrase=foobar&streamid=" + url.QueryEscape("123456,mode:publish,token:bla")
u, err = Parse(srturl)
require.NoError(t, err)
require.Equal(t, "srt", u.Scheme)
require.Equal(t, "127.0.0.1:6000", u.Host)
require.Equal(t, "123456,mode:publish,token:bla", u.StreamId)
si, err = u.StreamInfo()
require.NoError(t, err)
require.Equal(t, "publish", si.Mode)
require.Equal(t, "123456", si.Resource)
require.Equal(t, "bla", si.Token)
require.Equal(t, srturl, u.String())
}
func TestParseStreamId(t *testing.T) {
streamids := map[string]StreamInfo{
"": {Mode: "request"},
"bla": {Mode: "request", Resource: "bla"},
"bla,token=foobar": {Mode: "request", Resource: "bla,token=foobar"},
"bla,token:foobar": {Mode: "request", Resource: "bla", Token: "foobar"},
"bla,token:foobar,mode:publish": {Mode: "publish", Resource: "bla", Token: "foobar"},
"#!:": {Mode: "request"},
"#!:key=value": {Mode: "request"},
"#!:m=publish": {Mode: "publish"},
"#!:r=123456789": {Mode: "request", Resource: "123456789"},
"#!:token=foobar": {Mode: "request", Token: "foobar"},
"#!:token=foo,bar": {Mode: "request", Token: "foo"},
"#!:m=publish,r=123456789,token=foobar": {Mode: "publish", Resource: "123456789", Token: "foobar"},
}
for streamid, wantsi := range streamids {
si, err := ParseStreamId(streamid)
require.NoError(t, err)
require.Equal(t, wantsi, si, streamid)
}
}