mirror of
https://github.com/bluenviron/mediamtx
synced 2024-12-11 17:28:55 +00:00
implement path-based configuration
This commit is contained in:
parent
731cd9830e
commit
5010b4e69e
80
README.md
80
README.md
@ -56,19 +56,24 @@ Download and launch the image:
|
||||
docker run --rm -it --network=host aler9/rtsp-simple-server
|
||||
```
|
||||
|
||||
The `--network=host` argument is mandatory since Docker can change the source port of UDP packets for routing reasons, and this makes RTSP routing impossible. An alternative consists in disabling UDP and exposing the RTSP port, by providing a configuration file:
|
||||
```
|
||||
docker run --rm -it -p 8554:8554 aler9/rtsp-simple-server stdin << EOF
|
||||
The `--network=host` argument is mandatory since Docker can change the source port of UDP packets for routing reasons, and this makes RTSP routing impossible. An alternative consists in disabling UDP and exposing the RTSP port, by creating a configuration file named `conf.yml` with the following content:
|
||||
```yaml
|
||||
protocols: [tcp]
|
||||
EOF
|
||||
```
|
||||
|
||||
and passing it to the container:
|
||||
```
|
||||
docker run --rm -it -v $PWD/conf.yml:/conf.yml -p 8554:8554 aler9/rtsp-simple-server
|
||||
```
|
||||
|
||||
#### Publisher authentication
|
||||
|
||||
Create a file named `conf.yml` in the same folder of the executable, with the following content:
|
||||
```yaml
|
||||
publishUser: admin
|
||||
publishPass: mypassword
|
||||
paths:
|
||||
all:
|
||||
publishUser: admin
|
||||
publishPass: mypassword
|
||||
```
|
||||
|
||||
Start the server:
|
||||
@ -77,9 +82,21 @@ Start the server:
|
||||
```
|
||||
|
||||
Only publishers that provide both username and password will be able to publish:
|
||||
```
|
||||
ffmpeg -re -stream_loop -1 -i file.ts -c copy -f rtsp rtsp://admin:mypassword@localhost:8554/mystream
|
||||
```
|
||||
```
|
||||
ffmpeg -re -stream_loop -1 -i file.ts -c copy -f rtsp rtsp://admin:mypassword@localhost:8554/mystream
|
||||
```
|
||||
|
||||
It's also possible to set different credentials for each path:
|
||||
```yaml
|
||||
paths:
|
||||
path1:
|
||||
publishUser: admin
|
||||
publishPass: mypassword
|
||||
|
||||
path2:
|
||||
publishUser: admin
|
||||
publishPass: mypassword
|
||||
```
|
||||
|
||||
WARNING: RTSP is a plain protocol, and the credentials can be intercepted and read by malicious users (even if hashed, since the only supported hash method is md5, which is broken). If you need a secure channel, use RTSP inside a VPN.
|
||||
|
||||
@ -101,7 +118,7 @@ The current number of clients, publishers and receivers is printed in each log l
|
||||
|
||||
means that there are 2 clients, 1 publisher and 1 receiver.
|
||||
|
||||
#### Full configuration file
|
||||
#### Full configuration file (conf.yml)
|
||||
|
||||
```yaml
|
||||
# supported stream protocols (the handshake is always performed with TCP)
|
||||
@ -112,32 +129,35 @@ rtspPort: 8554
|
||||
rtpPort: 8000
|
||||
# port of the UDP rtcp listener
|
||||
rtcpPort: 8001
|
||||
|
||||
# username required to publish
|
||||
publishUser:
|
||||
# password required to publish
|
||||
publishPass:
|
||||
# IPs or networks (x.x.x.x/24) allowed to publish
|
||||
publishIps: []
|
||||
|
||||
# username required to read
|
||||
readUser:
|
||||
# password required to read
|
||||
readPass:
|
||||
# IPs or networks (x.x.x.x/24) allowed to read
|
||||
readIps: []
|
||||
|
||||
# script to run when a client connects
|
||||
preScript:
|
||||
# script to run when a client disconnects
|
||||
postScript:
|
||||
|
||||
# timeout of read operations
|
||||
readTimeout: 5s
|
||||
# timeout of write operations
|
||||
writeTimeout: 5s
|
||||
# script to run when a client connects
|
||||
preScript:
|
||||
# script to run when a client disconnects
|
||||
postScript:
|
||||
# enable pprof on port 9999 to monitor performance
|
||||
pprof: false
|
||||
|
||||
# these settings are path-dependent. The settings under the path 'all' are
|
||||
# applied to all paths that does not match another path in the map.
|
||||
paths:
|
||||
all:
|
||||
# username required to publish
|
||||
publishUser:
|
||||
# password required to publish
|
||||
publishPass:
|
||||
# IPs or networks (x.x.x.x/24) allowed to publish
|
||||
publishIps: []
|
||||
|
||||
# username required to read
|
||||
readUser:
|
||||
# password required to read
|
||||
readPass:
|
||||
# IPs or networks (x.x.x.x/24) allowed to read
|
||||
readIps: []
|
||||
|
||||
```
|
||||
|
||||
#### Full command-line usage
|
||||
|
129
main.go
129
main.go
@ -172,26 +172,32 @@ type programEventTerminate struct{}
|
||||
|
||||
func (programEventTerminate) isProgramEvent() {}
|
||||
|
||||
type conf struct {
|
||||
Protocols []string `yaml:"protocols"`
|
||||
RtspPort int `yaml:"rtspPort"`
|
||||
RtpPort int `yaml:"rtpPort"`
|
||||
RtcpPort int `yaml:"rtcpPort"`
|
||||
PublishUser string `yaml:"publishUser"`
|
||||
PublishPass string `yaml:"publishPass"`
|
||||
PublishIps []string `yaml:"publishIps"`
|
||||
ReadUser string `yaml:"readUser"`
|
||||
ReadPass string `yaml:"readPass"`
|
||||
ReadIps []string `yaml:"readIps"`
|
||||
PreScript string `yaml:"preScript"`
|
||||
PostScript string `yaml:"postScript"`
|
||||
ReadTimeout time.Duration `yaml:"readTimeout"`
|
||||
WriteTimeout time.Duration `yaml:"writeTimeout"`
|
||||
Pprof bool `yaml:"pprof"`
|
||||
type ConfPath struct {
|
||||
PublishUser string `yaml:"publishUser"`
|
||||
PublishPass string `yaml:"publishPass"`
|
||||
PublishIps []string `yaml:"publishIps"`
|
||||
publishIps []interface{}
|
||||
ReadUser string `yaml:"readUser"`
|
||||
ReadPass string `yaml:"readPass"`
|
||||
ReadIps []string `yaml:"readIps"`
|
||||
readIps []interface{}
|
||||
}
|
||||
|
||||
func loadConf(confPath string, stdin io.Reader) (*conf, error) {
|
||||
if confPath == "stdin" {
|
||||
type conf struct {
|
||||
Protocols []string `yaml:"protocols"`
|
||||
RtspPort int `yaml:"rtspPort"`
|
||||
RtpPort int `yaml:"rtpPort"`
|
||||
RtcpPort int `yaml:"rtcpPort"`
|
||||
ReadTimeout time.Duration `yaml:"readTimeout"`
|
||||
WriteTimeout time.Duration `yaml:"writeTimeout"`
|
||||
PreScript string `yaml:"preScript"`
|
||||
PostScript string `yaml:"postScript"`
|
||||
Pprof bool `yaml:"pprof"`
|
||||
Paths map[string]*ConfPath `yaml:"paths"`
|
||||
}
|
||||
|
||||
func loadConf(fpath string, stdin io.Reader) (*conf, error) {
|
||||
if fpath == "stdin" {
|
||||
var ret conf
|
||||
err := yaml.NewDecoder(stdin).Decode(&ret)
|
||||
if err != nil {
|
||||
@ -202,13 +208,13 @@ func loadConf(confPath string, stdin io.Reader) (*conf, error) {
|
||||
|
||||
} else {
|
||||
// conf.yml is optional
|
||||
if confPath == "conf.yml" {
|
||||
if _, err := os.Stat(confPath); err != nil {
|
||||
if fpath == "conf.yml" {
|
||||
if _, err := os.Stat(fpath); err != nil {
|
||||
return &conf{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
f, err := os.Open(confPath)
|
||||
f, err := os.Open(fpath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -227,8 +233,6 @@ func loadConf(confPath string, stdin io.Reader) (*conf, error) {
|
||||
type program struct {
|
||||
conf *conf
|
||||
protocols map[streamProtocol]struct{}
|
||||
publishIps []interface{}
|
||||
readIps []interface{}
|
||||
tcpl *serverTcpListener
|
||||
udplRtp *serverUdpListener
|
||||
udplRtcp *serverUdpListener
|
||||
@ -242,13 +246,13 @@ type program struct {
|
||||
}
|
||||
|
||||
func newProgram(sargs []string, stdin io.Reader) (*program, error) {
|
||||
kingpin.CommandLine.Help = "rtsp-simple-server " + Version + "\n\n" +
|
||||
"RTSP server."
|
||||
k := kingpin.New("rtsp-simple-server",
|
||||
"rtsp-simple-server "+Version+"\n\nRTSP server.")
|
||||
|
||||
argVersion := kingpin.Flag("version", "print version").Bool()
|
||||
argConfPath := kingpin.Arg("confpath", "path to a config file. The default is conf.yml. Use 'stdin' to read config from stdin").Default("conf.yml").String()
|
||||
argVersion := k.Flag("version", "print version").Bool()
|
||||
argConfPath := k.Arg("confpath", "path to a config file. The default is conf.yml. Use 'stdin' to read config from stdin").Default("conf.yml").String()
|
||||
|
||||
kingpin.MustParse(kingpin.CommandLine.Parse(sargs))
|
||||
kingpin.MustParse(k.Parse(sargs))
|
||||
|
||||
if *argVersion == true {
|
||||
fmt.Println(Version)
|
||||
@ -290,7 +294,6 @@ func newProgram(sargs []string, stdin io.Reader) (*program, error) {
|
||||
if conf.RtspPort == 0 {
|
||||
conf.RtspPort = 8554
|
||||
}
|
||||
|
||||
if conf.RtpPort == 0 {
|
||||
conf.RtpPort = 8000
|
||||
}
|
||||
@ -304,47 +307,53 @@ func newProgram(sargs []string, stdin io.Reader) (*program, error) {
|
||||
return nil, fmt.Errorf("rtcp and rtp ports must be consecutive")
|
||||
}
|
||||
|
||||
if conf.PublishUser != "" {
|
||||
if !regexp.MustCompile("^[a-zA-Z0-9]+$").MatchString(conf.PublishUser) {
|
||||
return nil, fmt.Errorf("publish username must be alphanumeric")
|
||||
if len(conf.Paths) == 0 {
|
||||
conf.Paths = map[string]*ConfPath{
|
||||
"all": {},
|
||||
}
|
||||
}
|
||||
if conf.PublishPass != "" {
|
||||
if !regexp.MustCompile("^[a-zA-Z0-9]+$").MatchString(conf.PublishPass) {
|
||||
return nil, fmt.Errorf("publish password must be alphanumeric")
|
||||
}
|
||||
}
|
||||
publishIps, err := parseIpCidrList(conf.PublishIps)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if conf.ReadUser != "" && conf.ReadPass == "" || conf.ReadUser == "" && conf.ReadPass != "" {
|
||||
return nil, fmt.Errorf("read username and password must be both filled")
|
||||
}
|
||||
if conf.ReadUser != "" {
|
||||
if !regexp.MustCompile("^[a-zA-Z0-9]+$").MatchString(conf.ReadUser) {
|
||||
return nil, fmt.Errorf("read username must be alphanumeric")
|
||||
for _, pconf := range conf.Paths {
|
||||
if pconf.PublishUser != "" {
|
||||
if !regexp.MustCompile("^[a-zA-Z0-9]+$").MatchString(pconf.PublishUser) {
|
||||
return nil, fmt.Errorf("publish username must be alphanumeric")
|
||||
}
|
||||
}
|
||||
}
|
||||
if conf.ReadPass != "" {
|
||||
if !regexp.MustCompile("^[a-zA-Z0-9]+$").MatchString(conf.ReadPass) {
|
||||
return nil, fmt.Errorf("read password must be alphanumeric")
|
||||
if pconf.PublishPass != "" {
|
||||
if !regexp.MustCompile("^[a-zA-Z0-9]+$").MatchString(pconf.PublishPass) {
|
||||
return nil, fmt.Errorf("publish password must be alphanumeric")
|
||||
}
|
||||
}
|
||||
pconf.publishIps, err = parseIpCidrList(pconf.PublishIps)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if pconf.ReadUser != "" && pconf.ReadPass == "" || pconf.ReadUser == "" && pconf.ReadPass != "" {
|
||||
return nil, fmt.Errorf("read username and password must be both filled")
|
||||
}
|
||||
if pconf.ReadUser != "" {
|
||||
if !regexp.MustCompile("^[a-zA-Z0-9]+$").MatchString(pconf.ReadUser) {
|
||||
return nil, fmt.Errorf("read username must be alphanumeric")
|
||||
}
|
||||
}
|
||||
if pconf.ReadPass != "" {
|
||||
if !regexp.MustCompile("^[a-zA-Z0-9]+$").MatchString(pconf.ReadPass) {
|
||||
return nil, fmt.Errorf("read password must be alphanumeric")
|
||||
}
|
||||
}
|
||||
if pconf.ReadUser != "" && pconf.ReadPass == "" || pconf.ReadUser == "" && pconf.ReadPass != "" {
|
||||
return nil, fmt.Errorf("read username and password must be both filled")
|
||||
}
|
||||
pconf.readIps, err = parseIpCidrList(pconf.ReadIps)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if conf.ReadUser != "" && conf.ReadPass == "" || conf.ReadUser == "" && conf.ReadPass != "" {
|
||||
return nil, fmt.Errorf("read username and password must be both filled")
|
||||
}
|
||||
readIps, err := parseIpCidrList(conf.ReadIps)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
p := &program{
|
||||
conf: conf,
|
||||
protocols: protocols,
|
||||
publishIps: publishIps,
|
||||
readIps: readIps,
|
||||
clients: make(map[*serverClient]struct{}),
|
||||
publishers: make(map[string]*serverClient),
|
||||
events: make(chan programEvent),
|
||||
|
16
main_test.go
16
main_test.go
@ -140,9 +140,11 @@ func TestProtocols(t *testing.T) {
|
||||
|
||||
func TestPublishAuth(t *testing.T) {
|
||||
stdin := []byte("\n" +
|
||||
"publishUser: testuser\n" +
|
||||
"publishPass: testpass\n" +
|
||||
"publishIps: [172.17.0.0/16]\n")
|
||||
"paths:\n" +
|
||||
" all:\n" +
|
||||
" publishUser: testuser\n" +
|
||||
" publishPass: testpass\n" +
|
||||
" publishIps: [172.17.0.0/16]\n")
|
||||
p, err := newProgram([]string{"stdin"}, bytes.NewBuffer(stdin))
|
||||
require.NoError(t, err)
|
||||
defer p.close()
|
||||
@ -184,9 +186,11 @@ func TestPublishAuth(t *testing.T) {
|
||||
|
||||
func TestReadAuth(t *testing.T) {
|
||||
stdin := []byte("\n" +
|
||||
"readUser: testuser\n" +
|
||||
"readPass: testpass\n" +
|
||||
"readIps: [172.17.0.0/16]\n")
|
||||
"paths:\n" +
|
||||
" all:\n" +
|
||||
" readUser: testuser\n" +
|
||||
" readPass: testpass\n" +
|
||||
" readIps: [172.17.0.0/16]\n")
|
||||
p, err := newProgram([]string{"stdin"}, bytes.NewBuffer(stdin))
|
||||
require.NoError(t, err)
|
||||
defer p.close()
|
||||
|
@ -264,7 +264,7 @@ func (c *serverClient) validateAuth(req *gortsplib.Request, user string, pass st
|
||||
c.conn.WriteResponse(&gortsplib.Response{
|
||||
StatusCode: gortsplib.StatusUnauthorized,
|
||||
Header: gortsplib.Header{
|
||||
"CSeq": []string{req.Header["CSeq"][0]},
|
||||
"CSeq": req.Header["CSeq"],
|
||||
"WWW-Authenticate": (*auth).GenerateHeader(),
|
||||
},
|
||||
})
|
||||
@ -285,6 +285,18 @@ func (c *serverClient) validateAuth(req *gortsplib.Request, user string, pass st
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *serverClient) findConfForPath(path string) *ConfPath {
|
||||
if pconf, ok := c.p.conf.Paths[path]; ok {
|
||||
return pconf
|
||||
}
|
||||
|
||||
if pconf, ok := c.p.conf.Paths["all"]; ok {
|
||||
return pconf
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *serverClient) handleRequest(req *gortsplib.Request) bool {
|
||||
c.log(string(req.Method))
|
||||
|
||||
@ -339,7 +351,14 @@ func (c *serverClient) handleRequest(req *gortsplib.Request) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
err := c.validateAuth(req, c.p.conf.ReadUser, c.p.conf.ReadPass, &c.readAuth, c.p.readIps)
|
||||
pconf := c.findConfForPath(path)
|
||||
if pconf == nil {
|
||||
c.writeResError(req, gortsplib.StatusBadRequest,
|
||||
fmt.Errorf("unable to find a valid configuration for path '%s'", path))
|
||||
return false
|
||||
}
|
||||
|
||||
err := c.validateAuth(req, pconf.ReadUser, pconf.ReadPass, &c.readAuth, pconf.readIps)
|
||||
if err != nil {
|
||||
if err == errAuthCritical {
|
||||
return false
|
||||
@ -373,7 +392,14 @@ func (c *serverClient) handleRequest(req *gortsplib.Request) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
err := c.validateAuth(req, c.p.conf.PublishUser, c.p.conf.PublishPass, &c.publishAuth, c.p.publishIps)
|
||||
pconf := c.findConfForPath(path)
|
||||
if pconf == nil {
|
||||
c.writeResError(req, gortsplib.StatusBadRequest,
|
||||
fmt.Errorf("unable to find a valid configuration for path '%s'", path))
|
||||
return false
|
||||
}
|
||||
|
||||
err := c.validateAuth(req, pconf.PublishUser, pconf.PublishPass, &c.publishAuth, pconf.publishIps)
|
||||
if err != nil {
|
||||
if err == errAuthCritical {
|
||||
return false
|
||||
@ -436,7 +462,14 @@ func (c *serverClient) handleRequest(req *gortsplib.Request) bool {
|
||||
switch c.state {
|
||||
// play
|
||||
case _CLIENT_STATE_STARTING, _CLIENT_STATE_PRE_PLAY:
|
||||
err := c.validateAuth(req, c.p.conf.ReadUser, c.p.conf.ReadPass, &c.readAuth, c.p.readIps)
|
||||
pconf := c.findConfForPath(path)
|
||||
if pconf == nil {
|
||||
c.writeResError(req, gortsplib.StatusBadRequest,
|
||||
fmt.Errorf("unable to find a valid configuration for path '%s'", path))
|
||||
return false
|
||||
}
|
||||
|
||||
err := c.validateAuth(req, pconf.ReadUser, pconf.ReadPass, &c.readAuth, pconf.readIps)
|
||||
if err != nil {
|
||||
if err == errAuthCritical {
|
||||
return false
|
||||
|
Loading…
Reference in New Issue
Block a user