13 changed files with 905 additions and 446 deletions
@ -0,0 +1,175 @@ |
|||||||
|
package conf |
||||||
|
|
||||||
|
import ( |
||||||
|
"encoding/json" |
||||||
|
"fmt" |
||||||
|
"net/url" |
||||||
|
"regexp" |
||||||
|
"strings" |
||||||
|
|
||||||
|
"github.com/aler9/gortsplib" |
||||||
|
) |
||||||
|
|
||||||
|
type PathConf struct { |
||||||
|
Regexp *regexp.Regexp `yaml:"-" json:"-"` |
||||||
|
Source string `yaml:"source"` |
||||||
|
SourceProtocol string `yaml:"sourceProtocol"` |
||||||
|
SourceProtocolParsed gortsplib.StreamProtocol `yaml:"-" json:"-"` |
||||||
|
SourceOnDemand bool `yaml:"sourceOnDemand"` |
||||||
|
RunOnInit string `yaml:"runOnInit"` |
||||||
|
RunOnDemand string `yaml:"runOnDemand"` |
||||||
|
RunOnPublish string `yaml:"runOnPublish"` |
||||||
|
RunOnRead string `yaml:"runOnRead"` |
||||||
|
PublishUser string `yaml:"publishUser"` |
||||||
|
PublishPass string `yaml:"publishPass"` |
||||||
|
PublishIps []string `yaml:"publishIps"` |
||||||
|
PublishIpsParsed []interface{} `yaml:"-" json:"-"` |
||||||
|
ReadUser string `yaml:"readUser"` |
||||||
|
ReadPass string `yaml:"readPass"` |
||||||
|
ReadIps []string `yaml:"readIps"` |
||||||
|
ReadIpsParsed []interface{} `yaml:"-" json:"-"` |
||||||
|
} |
||||||
|
|
||||||
|
func (pconf *PathConf) fillAndCheck(name string) error { |
||||||
|
if name == "" { |
||||||
|
return fmt.Errorf("path name can not be empty") |
||||||
|
} |
||||||
|
|
||||||
|
// normal path
|
||||||
|
if name[0] != '~' { |
||||||
|
err := CheckPathName(name) |
||||||
|
if err != nil { |
||||||
|
return fmt.Errorf("invalid path name: %s (%s)", err, name) |
||||||
|
} |
||||||
|
|
||||||
|
// regular expression path
|
||||||
|
} else { |
||||||
|
pathRegexp, err := regexp.Compile(name[1:]) |
||||||
|
if err != nil { |
||||||
|
return fmt.Errorf("invalid regular expression: %s", name[1:]) |
||||||
|
} |
||||||
|
pconf.Regexp = pathRegexp |
||||||
|
} |
||||||
|
|
||||||
|
if pconf.Source == "" { |
||||||
|
pconf.Source = "record" |
||||||
|
} |
||||||
|
|
||||||
|
if strings.HasPrefix(pconf.Source, "rtsp://") { |
||||||
|
if pconf.Regexp != nil { |
||||||
|
return fmt.Errorf("a path with a regular expression (or path 'all') cannot have a RTSP source; use another path") |
||||||
|
} |
||||||
|
|
||||||
|
u, err := url.Parse(pconf.Source) |
||||||
|
if err != nil { |
||||||
|
return fmt.Errorf("'%s' is not a valid url", pconf.Source) |
||||||
|
} |
||||||
|
if u.User != nil { |
||||||
|
pass, _ := u.User.Password() |
||||||
|
user := u.User.Username() |
||||||
|
if user != "" && pass == "" || |
||||||
|
user == "" && pass != "" { |
||||||
|
fmt.Errorf("username and password must be both provided") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if pconf.SourceProtocol == "" { |
||||||
|
pconf.SourceProtocol = "udp" |
||||||
|
} |
||||||
|
switch pconf.SourceProtocol { |
||||||
|
case "udp": |
||||||
|
pconf.SourceProtocolParsed = gortsplib.StreamProtocolUDP |
||||||
|
|
||||||
|
case "tcp": |
||||||
|
pconf.SourceProtocolParsed = gortsplib.StreamProtocolTCP |
||||||
|
|
||||||
|
default: |
||||||
|
return fmt.Errorf("unsupported protocol '%s'", pconf.SourceProtocol) |
||||||
|
} |
||||||
|
|
||||||
|
} else if strings.HasPrefix(pconf.Source, "rtmp://") { |
||||||
|
if pconf.Regexp != nil { |
||||||
|
return fmt.Errorf("a path with a regular expression (or path 'all') cannot have a RTMP source; use another path") |
||||||
|
} |
||||||
|
|
||||||
|
u, err := url.Parse(pconf.Source) |
||||||
|
if err != nil { |
||||||
|
return fmt.Errorf("'%s' is not a valid url", pconf.Source) |
||||||
|
} |
||||||
|
if u.User != nil { |
||||||
|
pass, _ := u.User.Password() |
||||||
|
user := u.User.Username() |
||||||
|
if user != "" && pass == "" || |
||||||
|
user == "" && pass != "" { |
||||||
|
fmt.Errorf("username and password must be both provided") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} else if pconf.Source == "record" { |
||||||
|
|
||||||
|
} else { |
||||||
|
return fmt.Errorf("unsupported source: '%s'", pconf.Source) |
||||||
|
} |
||||||
|
|
||||||
|
if pconf.PublishUser != "" { |
||||||
|
if !regexp.MustCompile("^[a-zA-Z0-9]+$").MatchString(pconf.PublishUser) { |
||||||
|
return fmt.Errorf("publish username must be alphanumeric") |
||||||
|
} |
||||||
|
} |
||||||
|
if pconf.PublishPass != "" { |
||||||
|
if !regexp.MustCompile("^[a-zA-Z0-9]+$").MatchString(pconf.PublishPass) { |
||||||
|
return fmt.Errorf("publish password must be alphanumeric") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if len(pconf.PublishIps) > 0 { |
||||||
|
var err error |
||||||
|
pconf.PublishIpsParsed, err = parseIpCidrList(pconf.PublishIps) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
} else { |
||||||
|
// the configuration file doesn't use nil dicts - avoid test fails by using nil
|
||||||
|
pconf.PublishIps = nil |
||||||
|
} |
||||||
|
|
||||||
|
if pconf.ReadUser != "" && pconf.ReadPass == "" || pconf.ReadUser == "" && pconf.ReadPass != "" { |
||||||
|
return fmt.Errorf("read username and password must be both filled") |
||||||
|
} |
||||||
|
if pconf.ReadUser != "" { |
||||||
|
if !regexp.MustCompile("^[a-zA-Z0-9]+$").MatchString(pconf.ReadUser) { |
||||||
|
return fmt.Errorf("read username must be alphanumeric") |
||||||
|
} |
||||||
|
} |
||||||
|
if pconf.ReadPass != "" { |
||||||
|
if !regexp.MustCompile("^[a-zA-Z0-9]+$").MatchString(pconf.ReadPass) { |
||||||
|
return fmt.Errorf("read password must be alphanumeric") |
||||||
|
} |
||||||
|
} |
||||||
|
if pconf.ReadUser != "" && pconf.ReadPass == "" || pconf.ReadUser == "" && pconf.ReadPass != "" { |
||||||
|
return fmt.Errorf("read username and password must be both filled") |
||||||
|
} |
||||||
|
|
||||||
|
if len(pconf.ReadIps) > 0 { |
||||||
|
var err error |
||||||
|
pconf.ReadIpsParsed, err = parseIpCidrList(pconf.ReadIps) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
} else { |
||||||
|
// the configuration file doesn't use nil dicts - avoid test fails by using nil
|
||||||
|
pconf.ReadIps = nil |
||||||
|
} |
||||||
|
|
||||||
|
if pconf.Regexp != nil && pconf.RunOnInit != "" { |
||||||
|
return fmt.Errorf("a path with a regular expression does not support option 'runOnInit'; use another path") |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (pconf *PathConf) Equal(other *PathConf) bool { |
||||||
|
a, _ := json.Marshal(pconf) |
||||||
|
b, _ := json.Marshal(pconf) |
||||||
|
return string(a) == string(b) |
||||||
|
} |
||||||
@ -0,0 +1,74 @@ |
|||||||
|
package confwatcher |
||||||
|
|
||||||
|
import ( |
||||||
|
"os" |
||||||
|
"time" |
||||||
|
|
||||||
|
"github.com/fsnotify/fsnotify" |
||||||
|
) |
||||||
|
|
||||||
|
type ConfWatcher struct { |
||||||
|
inner *fsnotify.Watcher |
||||||
|
|
||||||
|
// out
|
||||||
|
signal chan struct{} |
||||||
|
done chan struct{} |
||||||
|
} |
||||||
|
|
||||||
|
func New(confPath string) (*ConfWatcher, error) { |
||||||
|
inner, err := fsnotify.NewWatcher() |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
if _, err := os.Stat(confPath); err == nil { |
||||||
|
err := inner.Add(confPath) |
||||||
|
if err != nil { |
||||||
|
inner.Close() |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
w := &ConfWatcher{ |
||||||
|
inner: inner, |
||||||
|
signal: make(chan struct{}), |
||||||
|
done: make(chan struct{}), |
||||||
|
} |
||||||
|
|
||||||
|
go w.run() |
||||||
|
return w, nil |
||||||
|
} |
||||||
|
|
||||||
|
func (w *ConfWatcher) Close() { |
||||||
|
go func() { |
||||||
|
for range w.signal { |
||||||
|
} |
||||||
|
}() |
||||||
|
w.inner.Close() |
||||||
|
<-w.done |
||||||
|
} |
||||||
|
|
||||||
|
func (w *ConfWatcher) run() { |
||||||
|
defer close(w.done) |
||||||
|
|
||||||
|
outer: |
||||||
|
for { |
||||||
|
select { |
||||||
|
case event := <-w.inner.Events: |
||||||
|
if (event.Op & fsnotify.Write) == fsnotify.Write { |
||||||
|
// wait some additional time to avoid EOF
|
||||||
|
time.Sleep(10 * time.Millisecond) |
||||||
|
w.signal <- struct{}{} |
||||||
|
} |
||||||
|
|
||||||
|
case <-w.inner.Errors: |
||||||
|
break outer |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
close(w.signal) |
||||||
|
} |
||||||
|
|
||||||
|
func (w *ConfWatcher) Watch() chan struct{} { |
||||||
|
return w.signal |
||||||
|
} |
||||||
Loading…
Reference in new issue