golanggohlsrtmpwebrtcmedia-serverobs-studiortcprtmp-proxyrtmp-serverrtprtsprtsp-proxyrtsp-relayrtsp-serversrtstreamingwebrtc-proxy
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
237 lines
7.4 KiB
237 lines
7.4 KiB
package conf |
|
|
|
import ( |
|
"encoding/json" |
|
"fmt" |
|
"net/url" |
|
"regexp" |
|
"strings" |
|
"time" |
|
|
|
"github.com/aler9/gortsplib" |
|
"github.com/aler9/gortsplib/pkg/base" |
|
) |
|
|
|
var reUserPass = regexp.MustCompile("^[a-zA-Z0-9!\\$\\(\\)\\*\\+\\.;<=>\\[\\]\\^_\\-\\{\\}]+$") |
|
|
|
const userPassSupportedChars = "A-Z,0-9,!,$,(,),*,+,.,;,<,=,>,[,],^,_,-,{,}" |
|
|
|
// PathConf is a path configuration. |
|
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 = "automatic" |
|
} |
|
|
|
switch pconf.SourceProtocol { |
|
case "udp": |
|
v := gortsplib.StreamProtocolUDP |
|
pconf.SourceProtocolParsed = &v |
|
|
|
case "tcp": |
|
v := gortsplib.StreamProtocolTCP |
|
pconf.SourceProtocolParsed = &v |
|
|
|
case "automatic": |
|
|
|
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 |
|
} |
|
|
|
// Equal checks whether two PathConfs are equal. |
|
func (pconf *PathConf) Equal(other *PathConf) bool { |
|
a, _ := json.Marshal(pconf) |
|
b, _ := json.Marshal(other) |
|
return string(a) == string(b) |
|
}
|
|
|