package conf import ( "encoding/json" "fmt" "net/url" "regexp" "strings" "time" "github.com/aler9/gortsplib/pkg/base" ) const userPassSupportedChars = "A-Z,0-9,!,$,(,),*,+,.,;,<,=,>,[,],^,_,-,{,}" var reUserPass = regexp.MustCompile(`^[a-zA-Z0-9!\$\(\)\*\+\.;<=>\[\]\^_\-\{\}]+$`) var rePathName = regexp.MustCompile(`^[0-9a-zA-Z_\-/\.~]+$`) // IsValidPathName checks if a path name is valid. func IsValidPathName(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 alphanumeric characters, underscore, dot, tilde, minus or slash") } return nil } // PathConf is a path configuration. type PathConf struct { Regexp *regexp.Regexp `json:"-"` // source Source string `json:"source"` SourceProtocol SourceProtocol `json:"sourceProtocol"` SourceAnyPortEnable bool `json:"sourceAnyPortEnable"` SourceFingerprint string `json:"sourceFingerprint"` SourceOnDemand bool `json:"sourceOnDemand"` SourceOnDemandStartTimeout StringDuration `json:"sourceOnDemandStartTimeout"` SourceOnDemandCloseAfter StringDuration `json:"sourceOnDemandCloseAfter"` SourceRedirect string `json:"sourceRedirect"` DisablePublisherOverride bool `json:"disablePublisherOverride"` Fallback string `json:"fallback"` // authentication PublishUser string `json:"publishUser"` PublishPass string `json:"publishPass"` PublishIPs IPsOrNets `json:"publishIPs"` ReadUser string `json:"readUser"` ReadPass string `json:"readPass"` ReadIPs IPsOrNets `json:"readIPs"` // custom commands RunOnInit string `json:"runOnInit"` RunOnInitRestart bool `json:"runOnInitRestart"` RunOnDemand string `json:"runOnDemand"` RunOnDemandRestart bool `json:"runOnDemandRestart"` RunOnDemandStartTimeout StringDuration `json:"runOnDemandStartTimeout"` RunOnDemandCloseAfter StringDuration `json:"runOnDemandCloseAfter"` RunOnPublish string `json:"runOnPublish"` RunOnPublishRestart bool `json:"runOnPublishRestart"` RunOnRead string `json:"runOnRead"` RunOnReadRestart bool `json:"runOnReadRestart"` } func (pconf *PathConf) checkAndFillMissing(name string) error { if name == "" { return fmt.Errorf("path name can not be empty") } // normal path if name[0] != '~' { err := IsValidPathName(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 = "publisher" } switch { case pconf.Source == "publisher": 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 { return fmt.Errorf("'%s' is not a valid RTSP URL", pconf.Source) } if strings.HasPrefix(pconf.Source, "rtsps://") && pconf.SourceFingerprint == "" { return fmt.Errorf("sourceFingerprint is required with a RTSPS URL") } 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 { 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 != "" { return fmt.Errorf("username and password must be both provided") } } case strings.HasPrefix(pconf.Source, "http://") || strings.HasPrefix(pconf.Source, "https://"): if pconf.Regexp != nil { return fmt.Errorf("a path with a regular expression (or path 'all') cannot have a HLS source; use another path") } u, err := url.Parse(pconf.Source) if err != nil { return fmt.Errorf("'%s' is not a valid HLS URL", pconf.Source) } if u.Scheme != "http" && u.Scheme != "https" { return fmt.Errorf("'%s' is not a valid HLS URL", pconf.Source) } if u.User != nil { pass, _ := u.User.Password() user := u.User.Username() if user != "" && pass == "" || user == "" && pass != "" { return fmt.Errorf("username and password must be both provided") } } case 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) } default: return fmt.Errorf("invalid source: '%s'", pconf.Source) } if pconf.SourceOnDemand { if pconf.Source == "publisher" { return fmt.Errorf("'sourceOnDemand' is useless when source is 'publisher'") } } if pconf.SourceOnDemandStartTimeout == 0 { pconf.SourceOnDemandStartTimeout = 10 * StringDuration(time.Second) } if pconf.SourceOnDemandCloseAfter == 0 { pconf.SourceOnDemandCloseAfter = 10 * StringDuration(time.Second) } if pconf.Fallback != "" { if strings.HasPrefix(pconf.Fallback, "/") { err := IsValidPathName(pconf.Fallback[1:]) if err != nil { return fmt.Errorf("'%s': %s", pconf.Fallback, err) } } else { _, err := base.ParseURL(pconf.Fallback) if err != nil { return fmt.Errorf("'%s' is not a valid RTSP URL", pconf.Fallback) } } } 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 != "publisher" { return fmt.Errorf("'publishUser' is useless when source is not 'publisher'") } 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 != "publisher" { return fmt.Errorf("'publishPass' is useless when source is not 'publisher', since " + "the stream is not provided by a publisher, but by a fixed source") } if !strings.HasPrefix(pconf.PublishPass, "sha256:") && !reUserPass.MatchString(pconf.PublishPass) { return fmt.Errorf("publish password contains unsupported characters (supported are %s)", userPassSupportedChars) } } if len(pconf.PublishIPs) > 0 && pconf.Source != "publisher" { return fmt.Errorf("'publishIPs' is useless when source is not 'publisher', since " + "the stream is not provided by a publisher, but by a fixed source") } if (pconf.ReadUser != "" && pconf.ReadPass == "") || (pconf.ReadUser == "" && pconf.ReadPass != "") { return fmt.Errorf("read username and password must be both filled") } if pconf.ReadUser != "" { 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 != "" { if !strings.HasPrefix(pconf.ReadPass, "sha256:") && !reUserPass.MatchString(pconf.ReadPass) { return fmt.Errorf("read password contains unsupported characters (supported are %s)", userPassSupportedChars) } } 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 != "publisher" { return fmt.Errorf("'runOnPublish' is useless when source is not 'publisher', since " + "the stream is not provided by a publisher, but by a fixed source") } if pconf.RunOnDemand != "" && pconf.Source != "publisher" { return fmt.Errorf("'runOnDemand' can be used only when source is 'publisher'") } if pconf.RunOnDemandStartTimeout == 0 { pconf.RunOnDemandStartTimeout = 10 * StringDuration(time.Second) } if pconf.RunOnDemandCloseAfter == 0 { pconf.RunOnDemandCloseAfter = 10 * StringDuration(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) }