Skip to content

Commit

Permalink
implement path-based configuration
Browse files Browse the repository at this point in the history
  • Loading branch information
aler9 committed Jun 27, 2020
1 parent 731cd98 commit 5010b4e
Show file tree
Hide file tree
Showing 4 changed files with 164 additions and 98 deletions.
80 changes: 50 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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.
Expand All @@ -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)
Expand All @@ -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
Expand Down
125 changes: 67 additions & 58 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,26 +172,32 @@ type programEventTerminate struct{}

func (programEventTerminate) isProgramEvent() {}

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

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"`
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(confPath string, stdin io.Reader) (*conf, error) {
if confPath == "stdin" {
func loadConf(fpath string, stdin io.Reader) (*conf, error) {
if fpath == "stdin" {
var ret conf
err := yaml.NewDecoder(stdin).Decode(&ret)
if err != nil {
Expand All @@ -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
}
Expand All @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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
}
Expand All @@ -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")

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 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
}
}
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")
if pconf.ReadUser != "" && pconf.ReadPass == "" || pconf.ReadUser == "" && pconf.ReadPass != "" {
return nil, fmt.Errorf("read username and password must be both filled")
}
}
if conf.ReadPass != "" {
if !regexp.MustCompile("^[a-zA-Z0-9]+$").MatchString(conf.ReadPass) {
return nil, fmt.Errorf("read password must be alphanumeric")
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),
Expand Down
16 changes: 10 additions & 6 deletions main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand Down
Loading

0 comments on commit 5010b4e

Please sign in to comment.