mediamtx/internal/conf/path.go

321 lines
9.4 KiB
Go
Raw Normal View History

package conf
import (
"encoding/json"
"fmt"
"net"
"net/url"
"regexp"
"strings"
"time"
"github.com/aler9/gortsplib"
2020-11-15 16:56:54 +00:00
"github.com/aler9/gortsplib/pkg/base"
)
const userPassSupportedChars = "A-Z,0-9,!,$,(,),*,+,.,;,<,=,>,[,],^,_,-,{,}"
2020-12-05 19:42:59 +00:00
var reUserPass = regexp.MustCompile(`^[a-zA-Z0-9!\$\(\)\*\+\.;<=>\[\]\^_\-\{\}]+$`)
var rePathName = regexp.MustCompile(`^[0-9a-zA-Z_\-/]+$`)
func parseIPCidrList(in []string) ([]interface{}, error) {
if len(in) == 0 {
return nil, nil
}
var ret []interface{}
for _, t := range in {
_, ipnet, err := net.ParseCIDR(t)
if err == nil {
ret = append(ret, ipnet)
continue
}
ip := net.ParseIP(t)
if ip != nil {
ret = append(ret, ip)
continue
}
return nil, fmt.Errorf("unable to parse ip/network '%s'", t)
}
return ret, nil
}
// CheckPathName checks if a path name is valid.
func CheckPathName(name string) error {
if name == "" {
return fmt.Errorf("cannot be empty")
}
if name[0] == '/' {
return fmt.Errorf("can't begin with a slash")
}
if name[len(name)-1] == '/' {
return fmt.Errorf("can't end with a slash")
}
if !rePathName.MatchString(name) {
return fmt.Errorf("can contain only alfanumeric characters, underscore, minus or slash")
}
return nil
}
2020-11-05 11:30:25 +00:00
// PathConf is a path configuration.
type PathConf struct {
Regexp *regexp.Regexp `yaml:"-" json:"-"`
// source
Source string `yaml:"source"`
SourceProtocol string `yaml:"sourceProtocol"`
SourceProtocolParsed *gortsplib.StreamProtocol `yaml:"-" json:"-"`
SourceFingerprint string `yaml:"sourceFingerprint" json:"sourceFingerprint"`
SourceOnDemand bool `yaml:"sourceOnDemand"`
SourceOnDemandStartTimeout time.Duration `yaml:"sourceOnDemandStartTimeout"`
SourceOnDemandCloseAfter time.Duration `yaml:"sourceOnDemandCloseAfter"`
SourceRedirect string `yaml:"sourceRedirect"`
DisablePublisherOverride bool `yaml:"disablePublisherOverride"`
Fallback string `yaml:"fallback"`
2021-04-11 17:05:08 +00:00
// authentication
PublishUser string `yaml:"publishUser"`
PublishPass string `yaml:"publishPass"`
2021-05-07 21:07:31 +00:00
PublishIPs []string `yaml:"publishIps"`
PublishIPsParsed []interface{} `yaml:"-" json:"-"`
2021-04-11 17:05:08 +00:00
ReadUser string `yaml:"readUser"`
ReadPass string `yaml:"readPass"`
2021-05-07 21:07:31 +00:00
ReadIPs []string `yaml:"readIps"`
ReadIPsParsed []interface{} `yaml:"-" json:"-"`
2021-04-11 17:05:08 +00:00
// custom commands
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"`
}
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"
}
2021-03-20 13:14:41 +00:00
switch {
case pconf.Source == "record":
2021-03-20 13:14:41 +00:00
case strings.HasPrefix(pconf.Source, "rtsp://") ||
strings.HasPrefix(pconf.Source, "rtsps://"):
if pconf.Regexp != nil {
return fmt.Errorf("a path with a regular expression (or path 'all') cannot have a RTSP source; use another path")
}
_, err := base.ParseURL(pconf.Source)
if err != nil {
2021-05-09 16:25:25 +00:00
return fmt.Errorf("'%s' is not a valid RTSP URL", pconf.Source)
}
2020-10-27 23:29:53 +00:00
if pconf.SourceProtocol == "" {
pconf.SourceProtocol = "automatic"
}
2020-10-27 23:29:53 +00:00
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)
}
if strings.HasPrefix(pconf.Source, "rtsps://") && pconf.SourceFingerprint == "" {
return fmt.Errorf("sourceFingerprint is required with a RTSPS URL")
}
2021-03-20 13:14:41 +00:00
case 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 {
2021-05-09 16:25:25 +00:00
return fmt.Errorf("'%s' is not a valid RTMP URL", pconf.Source)
}
2020-11-01 18:09:47 +00:00
if u.Scheme != "rtmp" {
2021-05-09 16:25:25 +00:00
return fmt.Errorf("'%s' is not a valid RTMP URL", pconf.Source)
2020-11-01 18:09:47 +00:00
}
2020-10-27 23:29:53 +00:00
if u.User != nil {
pass, _ := u.User.Password()
user := u.User.Username()
if user != "" && pass == "" ||
user == "" && pass != "" {
2020-12-05 19:42:59 +00:00
return fmt.Errorf("username and password must be both provided")
}
}
2021-03-20 13:14:41 +00:00
case pconf.Source == "redirect":
if pconf.SourceRedirect == "" {
return fmt.Errorf("source redirect must be filled")
}
2020-10-27 23:29:53 +00:00
2020-11-01 18:09:47 +00:00
_, err := base.ParseURL(pconf.SourceRedirect)
2020-10-27 23:29:53 +00:00
if err != nil {
2021-05-09 16:25:25 +00:00
return fmt.Errorf("'%s' is not a valid RTSP URL", pconf.SourceRedirect)
2020-10-27 23:29:53 +00:00
}
2021-03-20 13:14:41 +00:00
default:
return fmt.Errorf("invalid source: '%s'", pconf.Source)
}
if pconf.SourceOnDemand {
2021-02-09 21:33:50 +00:00
if pconf.Source == "record" {
return fmt.Errorf("'sourceOnDemand' is useless when source is 'record'")
}
}
if pconf.SourceOnDemandStartTimeout == 0 {
pconf.SourceOnDemandStartTimeout = 10 * time.Second
}
if pconf.SourceOnDemandCloseAfter == 0 {
pconf.SourceOnDemandCloseAfter = 10 * time.Second
2020-10-27 23:29:53 +00:00
}
2020-11-01 16:48:00 +00:00
if pconf.Fallback != "" {
2021-02-09 21:33:50 +00:00
if strings.HasPrefix(pconf.Fallback, "/") {
err := CheckPathName(pconf.Fallback[1:])
if err != nil {
return fmt.Errorf("'%s': %s", pconf.Fallback, err)
}
} else {
_, err := base.ParseURL(pconf.Fallback)
if err != nil {
2021-05-09 16:25:25 +00:00
return fmt.Errorf("'%s' is not a valid RTSP URL", pconf.Fallback)
2021-02-09 21:33:50 +00:00
}
2020-11-01 16:48:00 +00:00
}
}
if (pconf.PublishUser != "" && pconf.PublishPass == "") || (pconf.PublishUser == "" && pconf.PublishPass != "") {
return fmt.Errorf("read username and password must be both filled")
}
if pconf.PublishUser != "" {
if pconf.Source != "record" {
2021-02-09 21:33:50 +00:00
return fmt.Errorf("'publishUser' is useless when source is not 'record'")
}
2020-12-31 18:47:25 +00:00
if !strings.HasPrefix(pconf.PublishUser, "sha256:") && !reUserPass.MatchString(pconf.PublishUser) {
return fmt.Errorf("publish username contains unsupported characters (supported are %s)", userPassSupportedChars)
}
}
if pconf.PublishPass != "" {
if pconf.Source != "record" {
return fmt.Errorf("'publishPass' is useless when source is not 'record', since the stream is not provided by a publisher, but by a fixed source")
}
2020-12-31 18:47:25 +00:00
if !strings.HasPrefix(pconf.PublishPass, "sha256:") && !reUserPass.MatchString(pconf.PublishPass) {
return fmt.Errorf("publish password contains unsupported characters (supported are %s)", userPassSupportedChars)
}
}
2021-05-07 21:07:31 +00:00
if len(pconf.PublishIPs) == 0 {
pconf.PublishIPs = nil
}
var err error
2021-05-07 21:07:31 +00:00
pconf.PublishIPsParsed, err = func() ([]interface{}, error) {
if len(pconf.PublishIPs) == 0 {
return nil, nil
}
if pconf.Source != "record" {
return nil, fmt.Errorf("'publishIps' is useless when source is not 'record', since the stream is not provided by a publisher, but by a fixed source")
}
2021-05-07 21:07:31 +00:00
return parseIPCidrList(pconf.PublishIPs)
}()
if err != nil {
return err
}
if (pconf.ReadUser != "" && pconf.ReadPass == "") || (pconf.ReadUser == "" && pconf.ReadPass != "") {
return fmt.Errorf("read username and password must be both filled")
}
if pconf.ReadUser != "" {
2020-12-31 18:47:25 +00:00
if !strings.HasPrefix(pconf.ReadUser, "sha256:") && !reUserPass.MatchString(pconf.ReadUser) {
return fmt.Errorf("read username contains unsupported characters (supported are %s)", userPassSupportedChars)
}
}
if pconf.ReadPass != "" {
2020-12-31 18:47:25 +00:00
if !strings.HasPrefix(pconf.ReadPass, "sha256:") && !reUserPass.MatchString(pconf.ReadPass) {
return fmt.Errorf("read password contains unsupported characters (supported are %s)", userPassSupportedChars)
}
}
2021-05-07 21:07:31 +00:00
if len(pconf.ReadIPs) == 0 {
pconf.ReadIPs = nil
}
2021-05-07 21:07:31 +00:00
pconf.ReadIPsParsed, err = func() ([]interface{}, error) {
return parseIPCidrList(pconf.ReadIPs)
}()
if err != nil {
return err
}
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.RunOnPublish != "" && pconf.Source != "record" {
return fmt.Errorf("'runOnPublish' is useless when source is not 'record', since the stream is not provided by a publisher, but by a fixed source")
}
if pconf.RunOnDemandStartTimeout == 0 {
pconf.RunOnDemandStartTimeout = 10 * time.Second
}
if pconf.RunOnDemandCloseAfter == 0 {
pconf.RunOnDemandCloseAfter = 10 * time.Second
}
return nil
}
2020-11-05 11:30:25 +00:00
// 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)
}