Browse Source

implement path-based configuration

pull/31/head v0.7.0
aler9 6 years ago
parent
commit
5010b4e69e
  1. 80
      README.md
  2. 77
      main.go
  3. 16
      main_test.go
  4. 41
      server-client.go

80
README.md

@ -56,19 +56,24 @@ Download and launch the image:
docker run --rm -it --network=host aler9/rtsp-simple-server 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: 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
docker run --rm -it -p 8554:8554 aler9/rtsp-simple-server stdin << EOF
protocols: [tcp] 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 #### Publisher authentication
Create a file named `conf.yml` in the same folder of the executable, with the following content: Create a file named `conf.yml` in the same folder of the executable, with the following content:
```yaml ```yaml
publishUser: admin paths:
publishPass: mypassword all:
publishUser: admin
publishPass: mypassword
``` ```
Start the server: Start the server:
@ -77,9 +82,21 @@ Start the server:
``` ```
Only publishers that provide both username and password will be able to publish: 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. 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. means that there are 2 clients, 1 publisher and 1 receiver.
#### Full configuration file #### Full configuration file (conf.yml)
```yaml ```yaml
# supported stream protocols (the handshake is always performed with TCP) # supported stream protocols (the handshake is always performed with TCP)
@ -112,32 +129,35 @@ rtspPort: 8554
rtpPort: 8000 rtpPort: 8000
# port of the UDP rtcp listener # port of the UDP rtcp listener
rtcpPort: 8001 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 # timeout of read operations
readTimeout: 5s readTimeout: 5s
# timeout of write operations # timeout of write operations
writeTimeout: 5s 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 # enable pprof on port 9999 to monitor performance
pprof: false 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 #### Full command-line usage

77
main.go

@ -172,26 +172,32 @@ type programEventTerminate struct{}
func (programEventTerminate) isProgramEvent() {} func (programEventTerminate) isProgramEvent() {}
type conf struct { type ConfPath struct {
Protocols []string `yaml:"protocols"`
RtspPort int `yaml:"rtspPort"`
RtpPort int `yaml:"rtpPort"`
RtcpPort int `yaml:"rtcpPort"`
PublishUser string `yaml:"publishUser"` PublishUser string `yaml:"publishUser"`
PublishPass string `yaml:"publishPass"` PublishPass string `yaml:"publishPass"`
PublishIps []string `yaml:"publishIps"` PublishIps []string `yaml:"publishIps"`
publishIps []interface{}
ReadUser string `yaml:"readUser"` ReadUser string `yaml:"readUser"`
ReadPass string `yaml:"readPass"` ReadPass string `yaml:"readPass"`
ReadIps []string `yaml:"readIps"` ReadIps []string `yaml:"readIps"`
PreScript string `yaml:"preScript"` readIps []interface{}
PostScript string `yaml:"postScript"` }
type conf struct {
Protocols []string `yaml:"protocols"`
RtspPort int `yaml:"rtspPort"`
RtpPort int `yaml:"rtpPort"`
RtcpPort int `yaml:"rtcpPort"`
ReadTimeout time.Duration `yaml:"readTimeout"` ReadTimeout time.Duration `yaml:"readTimeout"`
WriteTimeout time.Duration `yaml:"writeTimeout"` WriteTimeout time.Duration `yaml:"writeTimeout"`
PreScript string `yaml:"preScript"`
PostScript string `yaml:"postScript"`
Pprof bool `yaml:"pprof"` Pprof bool `yaml:"pprof"`
Paths map[string]*ConfPath `yaml:"paths"`
} }
func loadConf(confPath string, stdin io.Reader) (*conf, error) { func loadConf(fpath string, stdin io.Reader) (*conf, error) {
if confPath == "stdin" { if fpath == "stdin" {
var ret conf var ret conf
err := yaml.NewDecoder(stdin).Decode(&ret) err := yaml.NewDecoder(stdin).Decode(&ret)
if err != nil { if err != nil {
@ -202,13 +208,13 @@ func loadConf(confPath string, stdin io.Reader) (*conf, error) {
} else { } else {
// conf.yml is optional // conf.yml is optional
if confPath == "conf.yml" { if fpath == "conf.yml" {
if _, err := os.Stat(confPath); err != nil { if _, err := os.Stat(fpath); err != nil {
return &conf{}, nil return &conf{}, nil
} }
} }
f, err := os.Open(confPath) f, err := os.Open(fpath)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -227,8 +233,6 @@ func loadConf(confPath string, stdin io.Reader) (*conf, error) {
type program struct { type program struct {
conf *conf conf *conf
protocols map[streamProtocol]struct{} protocols map[streamProtocol]struct{}
publishIps []interface{}
readIps []interface{}
tcpl *serverTcpListener tcpl *serverTcpListener
udplRtp *serverUdpListener udplRtp *serverUdpListener
udplRtcp *serverUdpListener udplRtcp *serverUdpListener
@ -242,13 +246,13 @@ type program struct {
} }
func newProgram(sargs []string, stdin io.Reader) (*program, error) { func newProgram(sargs []string, stdin io.Reader) (*program, error) {
kingpin.CommandLine.Help = "rtsp-simple-server " + Version + "\n\n" + k := kingpin.New("rtsp-simple-server",
"RTSP server." "rtsp-simple-server "+Version+"\n\nRTSP server.")
argVersion := kingpin.Flag("version", "print version").Bool() argVersion := k.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() 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 { if *argVersion == true {
fmt.Println(Version) fmt.Println(Version)
@ -290,7 +294,6 @@ func newProgram(sargs []string, stdin io.Reader) (*program, error) {
if conf.RtspPort == 0 { if conf.RtspPort == 0 {
conf.RtspPort = 8554 conf.RtspPort = 8554
} }
if conf.RtpPort == 0 { if conf.RtpPort == 0 {
conf.RtpPort = 8000 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") return nil, fmt.Errorf("rtcp and rtp ports must be consecutive")
} }
if conf.PublishUser != "" { if len(conf.Paths) == 0 {
if !regexp.MustCompile("^[a-zA-Z0-9]+$").MatchString(conf.PublishUser) { conf.Paths = map[string]*ConfPath{
"all": {},
}
}
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") return nil, fmt.Errorf("publish username must be alphanumeric")
} }
} }
if conf.PublishPass != "" { if pconf.PublishPass != "" {
if !regexp.MustCompile("^[a-zA-Z0-9]+$").MatchString(conf.PublishPass) { if !regexp.MustCompile("^[a-zA-Z0-9]+$").MatchString(pconf.PublishPass) {
return nil, fmt.Errorf("publish password must be alphanumeric") return nil, fmt.Errorf("publish password must be alphanumeric")
} }
} }
publishIps, err := parseIpCidrList(conf.PublishIps) pconf.publishIps, err = parseIpCidrList(pconf.PublishIps)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if conf.ReadUser != "" && conf.ReadPass == "" || conf.ReadUser == "" && conf.ReadPass != "" { if pconf.ReadUser != "" && pconf.ReadPass == "" || pconf.ReadUser == "" && pconf.ReadPass != "" {
return nil, fmt.Errorf("read username and password must be both filled") return nil, fmt.Errorf("read username and password must be both filled")
} }
if conf.ReadUser != "" { if pconf.ReadUser != "" {
if !regexp.MustCompile("^[a-zA-Z0-9]+$").MatchString(conf.ReadUser) { if !regexp.MustCompile("^[a-zA-Z0-9]+$").MatchString(pconf.ReadUser) {
return nil, fmt.Errorf("read username must be alphanumeric") return nil, fmt.Errorf("read username must be alphanumeric")
} }
} }
if conf.ReadPass != "" { if pconf.ReadPass != "" {
if !regexp.MustCompile("^[a-zA-Z0-9]+$").MatchString(conf.ReadPass) { if !regexp.MustCompile("^[a-zA-Z0-9]+$").MatchString(pconf.ReadPass) {
return nil, fmt.Errorf("read password must be alphanumeric") return nil, fmt.Errorf("read password must be alphanumeric")
} }
} }
if conf.ReadUser != "" && conf.ReadPass == "" || conf.ReadUser == "" && conf.ReadPass != "" { if pconf.ReadUser != "" && pconf.ReadPass == "" || pconf.ReadUser == "" && pconf.ReadPass != "" {
return nil, fmt.Errorf("read username and password must be both filled") return nil, fmt.Errorf("read username and password must be both filled")
} }
readIps, err := parseIpCidrList(conf.ReadIps) pconf.readIps, err = parseIpCidrList(pconf.ReadIps)
if err != nil { if err != nil {
return nil, err return nil, err
} }
}
p := &program{ p := &program{
conf: conf, conf: conf,
protocols: protocols, protocols: protocols,
publishIps: publishIps,
readIps: readIps,
clients: make(map[*serverClient]struct{}), clients: make(map[*serverClient]struct{}),
publishers: make(map[string]*serverClient), publishers: make(map[string]*serverClient),
events: make(chan programEvent), events: make(chan programEvent),

16
main_test.go

@ -140,9 +140,11 @@ func TestProtocols(t *testing.T) {
func TestPublishAuth(t *testing.T) { func TestPublishAuth(t *testing.T) {
stdin := []byte("\n" + stdin := []byte("\n" +
"publishUser: testuser\n" + "paths:\n" +
"publishPass: testpass\n" + " all:\n" +
"publishIps: [172.17.0.0/16]\n") " publishUser: testuser\n" +
" publishPass: testpass\n" +
" publishIps: [172.17.0.0/16]\n")
p, err := newProgram([]string{"stdin"}, bytes.NewBuffer(stdin)) p, err := newProgram([]string{"stdin"}, bytes.NewBuffer(stdin))
require.NoError(t, err) require.NoError(t, err)
defer p.close() defer p.close()
@ -184,9 +186,11 @@ func TestPublishAuth(t *testing.T) {
func TestReadAuth(t *testing.T) { func TestReadAuth(t *testing.T) {
stdin := []byte("\n" + stdin := []byte("\n" +
"readUser: testuser\n" + "paths:\n" +
"readPass: testpass\n" + " all:\n" +
"readIps: [172.17.0.0/16]\n") " readUser: testuser\n" +
" readPass: testpass\n" +
" readIps: [172.17.0.0/16]\n")
p, err := newProgram([]string{"stdin"}, bytes.NewBuffer(stdin)) p, err := newProgram([]string{"stdin"}, bytes.NewBuffer(stdin))
require.NoError(t, err) require.NoError(t, err)
defer p.close() defer p.close()

41
server-client.go

@ -264,7 +264,7 @@ func (c *serverClient) validateAuth(req *gortsplib.Request, user string, pass st
c.conn.WriteResponse(&gortsplib.Response{ c.conn.WriteResponse(&gortsplib.Response{
StatusCode: gortsplib.StatusUnauthorized, StatusCode: gortsplib.StatusUnauthorized,
Header: gortsplib.Header{ Header: gortsplib.Header{
"CSeq": []string{req.Header["CSeq"][0]}, "CSeq": req.Header["CSeq"],
"WWW-Authenticate": (*auth).GenerateHeader(), "WWW-Authenticate": (*auth).GenerateHeader(),
}, },
}) })
@ -285,6 +285,18 @@ func (c *serverClient) validateAuth(req *gortsplib.Request, user string, pass st
return nil 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 { func (c *serverClient) handleRequest(req *gortsplib.Request) bool {
c.log(string(req.Method)) c.log(string(req.Method))
@ -339,7 +351,14 @@ func (c *serverClient) handleRequest(req *gortsplib.Request) bool {
return false 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 != nil {
if err == errAuthCritical { if err == errAuthCritical {
return false return false
@ -373,7 +392,14 @@ func (c *serverClient) handleRequest(req *gortsplib.Request) bool {
return false 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 != nil {
if err == errAuthCritical { if err == errAuthCritical {
return false return false
@ -436,7 +462,14 @@ func (c *serverClient) handleRequest(req *gortsplib.Request) bool {
switch c.state { switch c.state {
// play // play
case _CLIENT_STATE_STARTING, _CLIENT_STATE_PRE_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 != nil {
if err == errAuthCritical { if err == errAuthCritical {
return false return false

Loading…
Cancel
Save