13 changed files with 905 additions and 446 deletions
@ -0,0 +1,175 @@
@@ -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 @@
@@ -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