Ready-to-use SRT / WebRTC / RTSP / RTMP / LL-HLS media server and media proxy that allows to read, publish, proxy, record and playback video and audio streams.
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.
 
 
 
 
 
 

286 lines
8.8 KiB

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)
}