Browse Source

switch from arguments to a configuration file

pull/31/head
aler9 5 years ago
parent
commit
731cd9830e
  1. 80
      README.md
  2. 182
      main.go
  3. 22
      main_test.go
  4. 22
      server-client.go
  5. 4
      server-tcpl.go
  6. 2
      server-udpl.go

80
README.md

@ -56,19 +56,27 @@ Download and launch the image: @@ -56,19 +56,27 @@ 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:
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 --protocols=tcp
docker run --rm -it -p 8554:8554 aler9/rtsp-simple-server stdin << EOF
protocols: [tcp]
EOF
```
#### Publisher authentication
Start the server and set a username and a password:
Create a file named `conf.yml` in the same folder of the executable, with the following content:
```yaml
publishUser: admin
publishPass: mypassword
```
./rtsp-simple-server --publish-user=admin --publish-pass=mypassword
Start the server:
```
./rtsp-simple-server
```
Only publishers that know 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
```
@ -93,6 +101,45 @@ The current number of clients, publishers and receivers is printed in each log l @@ -93,6 +101,45 @@ 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
```yaml
# supported stream protocols (the handshake is always performed with TCP)
protocols: [udp, tcp]
# port of the TCP rtsp listener
rtspPort: 8554
# port of the UDP rtp listener
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
# enable pprof on port 9999 to monitor performance
pprof: false
```
#### Full command-line usage
```
@ -103,23 +150,12 @@ rtsp-simple-server v0.0.0 @@ -103,23 +150,12 @@ rtsp-simple-server v0.0.0
RTSP server.
Flags:
--help Show context-sensitive help (also try --help-long and --help-man).
--version print version
--protocols="udp,tcp" supported protocols
--rtsp-port=8554 port of the RTSP TCP listener
--rtp-port=8000 port of the RTP UDP listener
--rtcp-port=8001 port of the RTCP UDP listener
--read-timeout=5s timeout of read operations
--write-timeout=5s timeout of write operations
--publish-user="" optional username required to publish
--publish-pass="" optional password required to publish
--publish-ips="" comma-separated list of IPs or networks (x.x.x.x/24) that can publish
--read-user="" optional username required to read
--read-pass="" optional password required to read
--read-ips="" comma-separated list of IPs or networks (x.x.x.x/24) that can read
--pre-script="" optional script to run on client connect
--post-script="" optional script to run on client disconnect
--pprof enable pprof on port 9999 to monitor performance
--help Show context-sensitive help (also try --help-long and --help-man).
--version print version
Args:
[<confpath>] path to a config file. The default is conf.yml. Use 'stdin' to
read config from stdin
```
#### Compile and run from source

182
main.go

@ -2,28 +2,29 @@ package main @@ -2,28 +2,29 @@ package main
import (
"fmt"
"io"
"log"
"net"
"net/http"
_ "net/http/pprof"
"os"
"regexp"
"strings"
"time"
"gopkg.in/alecthomas/kingpin.v2"
"gopkg.in/yaml.v2"
"gortc.io/sdp"
)
var Version = "v0.0.0"
func parseIpCidrList(in string) ([]interface{}, error) {
if in == "" {
func parseIpCidrList(in []string) ([]interface{}, error) {
if len(in) == 0 {
return nil, nil
}
var ret []interface{}
for _, t := range strings.Split(in, ",") {
for _, t := range in {
_, ipnet, err := net.ParseCIDR(t)
if err == nil {
ret = append(ret, ipnet)
@ -171,27 +172,60 @@ type programEventTerminate struct{} @@ -171,27 +172,60 @@ type programEventTerminate struct{}
func (programEventTerminate) isProgramEvent() {}
type args struct {
version bool
protocolsStr string
rtspPort int
rtpPort int
rtcpPort int
readTimeout time.Duration
writeTimeout time.Duration
publishUser string
publishPass string
publishIps string
readUser string
readPass string
readIps string
preScript string
postScript string
pprof bool
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"`
}
func loadConf(confPath string, stdin io.Reader) (*conf, error) {
if confPath == "stdin" {
var ret conf
err := yaml.NewDecoder(stdin).Decode(&ret)
if err != nil {
return nil, err
}
return &ret, nil
} else {
// conf.yml is optional
if confPath == "conf.yml" {
if _, err := os.Stat(confPath); err != nil {
return &conf{}, nil
}
}
f, err := os.Open(confPath)
if err != nil {
return nil, err
}
defer f.Close()
var ret conf
err = yaml.NewDecoder(f).Decode(&ret)
if err != nil {
return nil, err
}
return &ret, nil
}
}
type program struct {
args args
conf *conf
protocols map[streamProtocol]struct{}
publishIps []interface{}
readIps []interface{}
@ -207,55 +241,37 @@ type program struct { @@ -207,55 +241,37 @@ type program struct {
done chan struct{}
}
func newProgram(sargs []string) (*program, error) {
func newProgram(sargs []string, stdin io.Reader) (*program, error) {
kingpin.CommandLine.Help = "rtsp-simple-server " + Version + "\n\n" +
"RTSP server."
argVersion := kingpin.Flag("version", "print version").Bool()
argProtocolsStr := kingpin.Flag("protocols", "supported protocols").Default("udp,tcp").String()
argRtspPort := kingpin.Flag("rtsp-port", "port of the RTSP TCP listener").Default("8554").Int()
argRtpPort := kingpin.Flag("rtp-port", "port of the RTP UDP listener").Default("8000").Int()
argRtcpPort := kingpin.Flag("rtcp-port", "port of the RTCP UDP listener").Default("8001").Int()
argReadTimeout := kingpin.Flag("read-timeout", "timeout of read operations").Default("5s").Duration()
argWriteTimeout := kingpin.Flag("write-timeout", "timeout of write operations").Default("5s").Duration()
argPublishUser := kingpin.Flag("publish-user", "optional username required to publish").Default("").String()
argPublishPass := kingpin.Flag("publish-pass", "optional password required to publish").Default("").String()
argPublishIps := kingpin.Flag("publish-ips", "comma-separated list of IPs or networks (x.x.x.x/24) that can publish").Default("").String()
argReadUser := kingpin.Flag("read-user", "optional username required to read").Default("").String()
argReadPass := kingpin.Flag("read-pass", "optional password required to read").Default("").String()
argReadIps := kingpin.Flag("read-ips", "comma-separated list of IPs or networks (x.x.x.x/24) that can read").Default("").String()
argPreScript := kingpin.Flag("pre-script", "optional script to run on client connect").Default("").String()
argPostScript := kingpin.Flag("post-script", "optional script to run on client disconnect").Default("").String()
argPprof := kingpin.Flag("pprof", "enable pprof on port 9999 to monitor performance").Default("false").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()
kingpin.MustParse(kingpin.CommandLine.Parse(sargs))
args := args{
version: *argVersion,
protocolsStr: *argProtocolsStr,
rtspPort: *argRtspPort,
rtpPort: *argRtpPort,
rtcpPort: *argRtcpPort,
readTimeout: *argReadTimeout,
writeTimeout: *argWriteTimeout,
publishUser: *argPublishUser,
publishPass: *argPublishPass,
publishIps: *argPublishIps,
readUser: *argReadUser,
readPass: *argReadPass,
readIps: *argReadIps,
preScript: *argPreScript,
postScript: *argPostScript,
pprof: *argPprof,
}
if args.version == true {
if *argVersion == true {
fmt.Println(Version)
os.Exit(0)
}
conf, err := loadConf(*argConfPath, stdin)
if err != nil {
return nil, err
}
if conf.ReadTimeout == 0 {
conf.ReadTimeout = 5 * time.Second
}
if conf.WriteTimeout == 0 {
conf.WriteTimeout = 5 * time.Second
}
if len(conf.Protocols) == 0 {
conf.Protocols = []string{"udp", "tcp"}
}
protocols := make(map[streamProtocol]struct{})
for _, proto := range strings.Split(args.protocolsStr, ",") {
for _, proto := range conf.Protocols {
switch proto {
case "udp":
protocols[_STREAM_PROTOCOL_UDP] = struct{}{}
@ -271,51 +287,61 @@ func newProgram(sargs []string) (*program, error) { @@ -271,51 +287,61 @@ func newProgram(sargs []string) (*program, error) {
return nil, fmt.Errorf("no protocols provided")
}
if (args.rtpPort % 2) != 0 {
if conf.RtspPort == 0 {
conf.RtspPort = 8554
}
if conf.RtpPort == 0 {
conf.RtpPort = 8000
}
if (conf.RtpPort % 2) != 0 {
return nil, fmt.Errorf("rtp port must be even")
}
if args.rtcpPort != (args.rtpPort + 1) {
if conf.RtcpPort == 0 {
conf.RtcpPort = 8001
}
if conf.RtcpPort != (conf.RtpPort + 1) {
return nil, fmt.Errorf("rtcp and rtp ports must be consecutive")
}
if args.publishUser != "" {
if !regexp.MustCompile("^[a-zA-Z0-9]+$").MatchString(args.publishUser) {
if conf.PublishUser != "" {
if !regexp.MustCompile("^[a-zA-Z0-9]+$").MatchString(conf.PublishUser) {
return nil, fmt.Errorf("publish username must be alphanumeric")
}
}
if args.publishPass != "" {
if !regexp.MustCompile("^[a-zA-Z0-9]+$").MatchString(args.publishPass) {
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(args.publishIps)
publishIps, err := parseIpCidrList(conf.PublishIps)
if err != nil {
return nil, err
}
if args.readUser != "" && args.readPass == "" || args.readUser == "" && args.readPass != "" {
if conf.ReadUser != "" && conf.ReadPass == "" || conf.ReadUser == "" && conf.ReadPass != "" {
return nil, fmt.Errorf("read username and password must be both filled")
}
if args.readUser != "" {
if !regexp.MustCompile("^[a-zA-Z0-9]+$").MatchString(args.readUser) {
if conf.ReadUser != "" {
if !regexp.MustCompile("^[a-zA-Z0-9]+$").MatchString(conf.ReadUser) {
return nil, fmt.Errorf("read username must be alphanumeric")
}
}
if args.readPass != "" {
if !regexp.MustCompile("^[a-zA-Z0-9]+$").MatchString(args.readPass) {
if conf.ReadPass != "" {
if !regexp.MustCompile("^[a-zA-Z0-9]+$").MatchString(conf.ReadPass) {
return nil, fmt.Errorf("read password must be alphanumeric")
}
}
if args.readUser != "" && args.readPass == "" || args.readUser == "" && args.readPass != "" {
if conf.ReadUser != "" && conf.ReadPass == "" || conf.ReadUser == "" && conf.ReadPass != "" {
return nil, fmt.Errorf("read username and password must be both filled")
}
readIps, err := parseIpCidrList(args.readIps)
readIps, err := parseIpCidrList(conf.ReadIps)
if err != nil {
return nil, err
}
p := &program{
args: args,
conf: conf,
protocols: protocols,
publishIps: publishIps,
readIps: readIps,
@ -327,7 +353,7 @@ func newProgram(sargs []string) (*program, error) { @@ -327,7 +353,7 @@ func newProgram(sargs []string) (*program, error) {
p.log("rtsp-simple-server %s", Version)
if args.pprof {
if conf.Pprof {
go func(mux *http.ServeMux) {
server := &http.Server{
Addr: ":9999",
@ -339,12 +365,12 @@ func newProgram(sargs []string) (*program, error) { @@ -339,12 +365,12 @@ func newProgram(sargs []string) (*program, error) {
http.DefaultServeMux = http.NewServeMux()
}
p.udplRtp, err = newServerUdpListener(p, args.rtpPort, _TRACK_FLOW_RTP)
p.udplRtp, err = newServerUdpListener(p, conf.RtpPort, _TRACK_FLOW_RTP)
if err != nil {
return nil, err
}
p.udplRtcp, err = newServerUdpListener(p, args.rtcpPort, _TRACK_FLOW_RTCP)
p.udplRtcp, err = newServerUdpListener(p, conf.RtcpPort, _TRACK_FLOW_RTCP)
if err != nil {
return nil, err
}
@ -607,7 +633,7 @@ func (p *program) forwardTrack(path string, id int, trackFlowType trackFlowType, @@ -607,7 +633,7 @@ func (p *program) forwardTrack(path string, id int, trackFlowType trackFlowType,
}
func main() {
_, err := newProgram(os.Args[1:])
_, err := newProgram(os.Args[1:], os.Stdin)
if err != nil {
log.Fatal("ERR: ", err)
}

22
main_test.go

@ -97,7 +97,7 @@ func TestProtocols(t *testing.T) { @@ -97,7 +97,7 @@ func TestProtocols(t *testing.T) {
{"tcp", "tcp"},
} {
t.Run(pair[0]+"_"+pair[1], func(t *testing.T) {
p, err := newProgram([]string{})
p, err := newProgram([]string{}, bytes.NewBuffer(nil))
require.NoError(t, err)
defer p.close()
@ -139,11 +139,11 @@ func TestProtocols(t *testing.T) { @@ -139,11 +139,11 @@ func TestProtocols(t *testing.T) {
}
func TestPublishAuth(t *testing.T) {
p, err := newProgram([]string{
"--publish-user=testuser",
"--publish-pass=testpass",
"--publish-ips=172.17.0.0/16",
})
stdin := []byte("\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()
@ -183,11 +183,11 @@ func TestPublishAuth(t *testing.T) { @@ -183,11 +183,11 @@ func TestPublishAuth(t *testing.T) {
}
func TestReadAuth(t *testing.T) {
p, err := newProgram([]string{
"--read-user=testuser",
"--read-pass=testpass",
"--read-ips=172.17.0.0/16",
})
stdin := []byte("\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()

22
server-client.go

@ -95,8 +95,8 @@ func newServerClient(p *program, nconn net.Conn) *serverClient { @@ -95,8 +95,8 @@ func newServerClient(p *program, nconn net.Conn) *serverClient {
p: p,
conn: gortsplib.NewConnServer(gortsplib.ConnServerConf{
NConn: nconn,
ReadTimeout: p.args.readTimeout,
WriteTimeout: p.args.writeTimeout,
ReadTimeout: p.conf.ReadTimeout,
WriteTimeout: p.conf.WriteTimeout,
}),
state: _CLIENT_STATE_STARTING,
readBuf1: make([]byte, 0, 512*1024),
@ -124,8 +124,8 @@ func (c *serverClient) zone() string { @@ -124,8 +124,8 @@ func (c *serverClient) zone() string {
}
func (c *serverClient) run() {
if c.p.args.preScript != "" {
preScript := exec.Command(c.p.args.preScript)
if c.p.conf.PreScript != "" {
preScript := exec.Command(c.p.conf.PreScript)
err := preScript.Run()
if err != nil {
c.log("ERR: %s", err)
@ -157,8 +157,8 @@ func (c *serverClient) run() { @@ -157,8 +157,8 @@ func (c *serverClient) run() {
}()
func() {
if c.p.args.postScript != "" {
postScript := exec.Command(c.p.args.postScript)
if c.p.conf.PostScript != "" {
postScript := exec.Command(c.p.conf.PostScript)
err := postScript.Run()
if err != nil {
c.log("ERR: %s", err)
@ -339,7 +339,7 @@ func (c *serverClient) handleRequest(req *gortsplib.Request) bool { @@ -339,7 +339,7 @@ func (c *serverClient) handleRequest(req *gortsplib.Request) bool {
return false
}
err := c.validateAuth(req, c.p.args.readUser, c.p.args.readPass, &c.readAuth, c.p.readIps)
err := c.validateAuth(req, c.p.conf.ReadUser, c.p.conf.ReadPass, &c.readAuth, c.p.readIps)
if err != nil {
if err == errAuthCritical {
return false
@ -373,7 +373,7 @@ func (c *serverClient) handleRequest(req *gortsplib.Request) bool { @@ -373,7 +373,7 @@ func (c *serverClient) handleRequest(req *gortsplib.Request) bool {
return false
}
err := c.validateAuth(req, c.p.args.publishUser, c.p.args.publishPass, &c.publishAuth, c.p.publishIps)
err := c.validateAuth(req, c.p.conf.PublishUser, c.p.conf.PublishPass, &c.publishAuth, c.p.publishIps)
if err != nil {
if err == errAuthCritical {
return false
@ -436,7 +436,7 @@ func (c *serverClient) handleRequest(req *gortsplib.Request) bool { @@ -436,7 +436,7 @@ 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.args.readUser, c.p.args.readPass, &c.readAuth, c.p.readIps)
err := c.validateAuth(req, c.p.conf.ReadUser, c.p.conf.ReadPass, &c.readAuth, c.p.readIps)
if err != nil {
if err == errAuthCritical {
return false
@ -493,7 +493,7 @@ func (c *serverClient) handleRequest(req *gortsplib.Request) bool { @@ -493,7 +493,7 @@ func (c *serverClient) handleRequest(req *gortsplib.Request) bool {
"RTP/AVP/UDP",
"unicast",
fmt.Sprintf("client_port=%d-%d", rtpPort, rtcpPort),
fmt.Sprintf("server_port=%d-%d", c.p.args.rtpPort, c.p.args.rtcpPort),
fmt.Sprintf("server_port=%d-%d", c.p.conf.RtpPort, c.p.conf.RtcpPort),
}, ";")},
"Session": []string{"12345678"},
},
@ -608,7 +608,7 @@ func (c *serverClient) handleRequest(req *gortsplib.Request) bool { @@ -608,7 +608,7 @@ func (c *serverClient) handleRequest(req *gortsplib.Request) bool {
"RTP/AVP/UDP",
"unicast",
fmt.Sprintf("client_port=%d-%d", rtpPort, rtcpPort),
fmt.Sprintf("server_port=%d-%d", c.p.args.rtpPort, c.p.args.rtcpPort),
fmt.Sprintf("server_port=%d-%d", c.p.conf.RtpPort, c.p.conf.RtcpPort),
}, ";")},
"Session": []string{"12345678"},
},

4
server-tcpl.go

@ -13,7 +13,7 @@ type serverTcpListener struct { @@ -13,7 +13,7 @@ type serverTcpListener struct {
func newServerTcpListener(p *program) (*serverTcpListener, error) {
nconn, err := net.ListenTCP("tcp", &net.TCPAddr{
Port: p.args.rtspPort,
Port: p.conf.RtspPort,
})
if err != nil {
return nil, err
@ -25,7 +25,7 @@ func newServerTcpListener(p *program) (*serverTcpListener, error) { @@ -25,7 +25,7 @@ func newServerTcpListener(p *program) (*serverTcpListener, error) {
done: make(chan struct{}),
}
l.log("opened on :%d", p.args.rtspPort)
l.log("opened on :%d", p.conf.RtspPort)
return l, nil
}

2
server-udpl.go

@ -62,7 +62,7 @@ func (l *serverUdpListener) log(format string, args ...interface{}) { @@ -62,7 +62,7 @@ func (l *serverUdpListener) log(format string, args ...interface{}) {
func (l *serverUdpListener) run() {
go func() {
for w := range l.writec {
l.nconn.SetWriteDeadline(time.Now().Add(l.p.args.writeTimeout))
l.nconn.SetWriteDeadline(time.Now().Add(l.p.conf.WriteTimeout))
l.nconn.WriteTo(w.buf, w.addr)
}
}()

Loading…
Cancel
Save