mirror of
https://github.com/bluenviron/mediamtx
synced 2024-12-16 11:44:50 +00:00
232 lines
7.3 KiB
Go
232 lines
7.3 KiB
Go
package conf
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/url"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/aler9/gortsplib"
|
|
"github.com/aler9/gortsplib/base"
|
|
)
|
|
|
|
var reUserPass = regexp.MustCompile("^[a-zA-Z0-9!\\$\\(\\)\\*\\+\\.;<=>\\[\\]\\^_\\-\\{\\}]+$")
|
|
|
|
const userPassSupportedChars = "A-Z,0-9,!,$,(,),*,+,.,;,<,=,>,[,],^,_,-,{,}"
|
|
|
|
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"`
|
|
SourceOnDemandStartTimeout time.Duration `yaml:"sourceOnDemandStartTimeout"`
|
|
SourceOnDemandCloseAfter time.Duration `yaml:"sourceOnDemandCloseAfter"`
|
|
SourceRedirect string `yaml:"sourceRedirect"`
|
|
Fallback string `yaml:"fallback"`
|
|
RunOnInit string `yaml:"runOnInit"`
|
|
RunOnInitRestart bool `yaml:"runOnInitRestart"`
|
|
RunOnDemand string `yaml:"runOnDemand"`
|
|
RunOnDemandRestart bool `yaml:"runOnDemandRestart"`
|
|
RunOnDemandStartTimeout time.Duration `yaml:"runOnDemandStartTimeout"`
|
|
RunOnDemandCloseAfter time.Duration `yaml:"runOnDemandCloseAfter"`
|
|
RunOnPublish string `yaml:"runOnPublish"`
|
|
RunOnPublishRestart bool `yaml:"runOnPublishRestart"`
|
|
RunOnRead string `yaml:"runOnRead"`
|
|
RunOnReadRestart bool `yaml:"runOnReadRestart"`
|
|
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 pconf.Source == "record" {
|
|
|
|
} else 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 := base.ParseURL(pconf.Source)
|
|
if err != nil {
|
|
return fmt.Errorf("'%s' is not a valid rtsp 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 rtmp url", pconf.Source)
|
|
}
|
|
if u.Scheme != "rtmp" {
|
|
return fmt.Errorf("'%s' is not a valid rtmp 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 == "redirect" {
|
|
if pconf.SourceRedirect == "" {
|
|
return fmt.Errorf("source redirect must be filled")
|
|
}
|
|
|
|
_, err := base.ParseURL(pconf.SourceRedirect)
|
|
if err != nil {
|
|
return fmt.Errorf("'%s' is not a valid rtsp url", pconf.SourceRedirect)
|
|
}
|
|
|
|
} else {
|
|
return fmt.Errorf("invalid source: '%s'", pconf.Source)
|
|
}
|
|
|
|
if pconf.SourceOnDemandStartTimeout == 0 {
|
|
pconf.SourceOnDemandStartTimeout = 10 * time.Second
|
|
}
|
|
|
|
if pconf.SourceOnDemandCloseAfter == 0 {
|
|
pconf.SourceOnDemandCloseAfter = 10 * time.Second
|
|
}
|
|
|
|
if pconf.Fallback != "" {
|
|
_, err := base.ParseURL(pconf.Fallback)
|
|
if err != nil {
|
|
return fmt.Errorf("'%s' is not a valid rtsp url", pconf.Fallback)
|
|
}
|
|
}
|
|
|
|
if pconf.PublishUser != "" {
|
|
if !reUserPass.MatchString(pconf.PublishUser) {
|
|
return fmt.Errorf("publish username contains unsupported characters (supported are %s)", userPassSupportedChars)
|
|
}
|
|
}
|
|
|
|
if pconf.PublishPass != "" {
|
|
if !reUserPass.MatchString(pconf.PublishPass) {
|
|
return fmt.Errorf("publish password contains unsupported characters (supported are %s)", userPassSupportedChars)
|
|
}
|
|
}
|
|
|
|
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 !reUserPass.MatchString(pconf.ReadUser) {
|
|
return fmt.Errorf("read username contains unsupported characters (supported are %s)", userPassSupportedChars)
|
|
}
|
|
}
|
|
if pconf.ReadPass != "" {
|
|
if !reUserPass.MatchString(pconf.ReadPass) {
|
|
return fmt.Errorf("read password contains unsupported characters (supported are %s)", userPassSupportedChars)
|
|
}
|
|
}
|
|
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.RunOnInit != "" && pconf.Regexp != nil {
|
|
return fmt.Errorf("a path with a regular expression does not support option 'runOnInit'; use another path")
|
|
}
|
|
|
|
if pconf.RunOnDemandStartTimeout == 0 {
|
|
pconf.RunOnDemandStartTimeout = 10 * time.Second
|
|
}
|
|
|
|
if pconf.RunOnDemandCloseAfter == 0 {
|
|
pconf.RunOnDemandCloseAfter = 10 * time.Second
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (pconf *PathConf) Equal(other *PathConf) bool {
|
|
a, _ := json.Marshal(pconf)
|
|
b, _ := json.Marshal(pconf)
|
|
return string(a) == string(b)
|
|
}
|