Browse Source

switch from arguments to a configuration file

pull/31/head
aler9 5 years ago
parent
commit
731cd9830e
  1. 76
      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

76
README.md

@ -56,19 +56,27 @@ 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: 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 #### 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 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
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
```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 #### Full command-line usage
``` ```
@ -105,21 +152,10 @@ RTSP server.
Flags: Flags:
--help Show context-sensitive help (also try --help-long and --help-man). --help Show context-sensitive help (also try --help-long and --help-man).
--version print version --version print version
--protocols="udp,tcp" supported protocols
--rtsp-port=8554 port of the RTSP TCP listener Args:
--rtp-port=8000 port of the RTP UDP listener [<confpath>] path to a config file. The default is conf.yml. Use 'stdin' to
--rtcp-port=8001 port of the RTCP UDP listener read config from stdin
--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
``` ```
#### Compile and run from source #### Compile and run from source

182
main.go

@ -2,28 +2,29 @@ package main
import ( import (
"fmt" "fmt"
"io"
"log" "log"
"net" "net"
"net/http" "net/http"
_ "net/http/pprof" _ "net/http/pprof"
"os" "os"
"regexp" "regexp"
"strings"
"time" "time"
"gopkg.in/alecthomas/kingpin.v2" "gopkg.in/alecthomas/kingpin.v2"
"gopkg.in/yaml.v2"
"gortc.io/sdp" "gortc.io/sdp"
) )
var Version = "v0.0.0" var Version = "v0.0.0"
func parseIpCidrList(in string) ([]interface{}, error) { func parseIpCidrList(in []string) ([]interface{}, error) {
if in == "" { if len(in) == 0 {
return nil, nil return nil, nil
} }
var ret []interface{} var ret []interface{}
for _, t := range strings.Split(in, ",") { for _, t := range in {
_, ipnet, err := net.ParseCIDR(t) _, ipnet, err := net.ParseCIDR(t)
if err == nil { if err == nil {
ret = append(ret, ipnet) ret = append(ret, ipnet)
@ -171,27 +172,60 @@ type programEventTerminate struct{}
func (programEventTerminate) isProgramEvent() {} func (programEventTerminate) isProgramEvent() {}
type args struct { type conf struct {
version bool Protocols []string `yaml:"protocols"`
protocolsStr string RtspPort int `yaml:"rtspPort"`
rtspPort int RtpPort int `yaml:"rtpPort"`
rtpPort int RtcpPort int `yaml:"rtcpPort"`
rtcpPort int PublishUser string `yaml:"publishUser"`
readTimeout time.Duration PublishPass string `yaml:"publishPass"`
writeTimeout time.Duration PublishIps []string `yaml:"publishIps"`
publishUser string ReadUser string `yaml:"readUser"`
publishPass string ReadPass string `yaml:"readPass"`
publishIps string ReadIps []string `yaml:"readIps"`
readUser string PreScript string `yaml:"preScript"`
readPass string PostScript string `yaml:"postScript"`
readIps string ReadTimeout time.Duration `yaml:"readTimeout"`
preScript string WriteTimeout time.Duration `yaml:"writeTimeout"`
postScript string Pprof bool `yaml:"pprof"`
pprof bool }
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 { type program struct {
args args conf *conf
protocols map[streamProtocol]struct{} protocols map[streamProtocol]struct{}
publishIps []interface{} publishIps []interface{}
readIps []interface{} readIps []interface{}
@ -207,55 +241,37 @@ type program struct {
done chan 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" + kingpin.CommandLine.Help = "rtsp-simple-server " + Version + "\n\n" +
"RTSP server." "RTSP server."
argVersion := kingpin.Flag("version", "print version").Bool() argVersion := kingpin.Flag("version", "print version").Bool()
argProtocolsStr := kingpin.Flag("protocols", "supported protocols").Default("udp,tcp").String() 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()
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()
kingpin.MustParse(kingpin.CommandLine.Parse(sargs)) kingpin.MustParse(kingpin.CommandLine.Parse(sargs))
args := args{ if *argVersion == true {
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 {
fmt.Println(Version) fmt.Println(Version)
os.Exit(0) 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{}) protocols := make(map[streamProtocol]struct{})
for _, proto := range strings.Split(args.protocolsStr, ",") { for _, proto := range conf.Protocols {
switch proto { switch proto {
case "udp": case "udp":
protocols[_STREAM_PROTOCOL_UDP] = struct{}{} protocols[_STREAM_PROTOCOL_UDP] = struct{}{}
@ -271,51 +287,61 @@ func newProgram(sargs []string) (*program, error) {
return nil, fmt.Errorf("no protocols provided") 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") 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") return nil, fmt.Errorf("rtcp and rtp ports must be consecutive")
} }
if args.publishUser != "" { if conf.PublishUser != "" {
if !regexp.MustCompile("^[a-zA-Z0-9]+$").MatchString(args.publishUser) { if !regexp.MustCompile("^[a-zA-Z0-9]+$").MatchString(conf.PublishUser) {
return nil, fmt.Errorf("publish username must be alphanumeric") return nil, fmt.Errorf("publish username must be alphanumeric")
} }
} }
if args.publishPass != "" { if conf.PublishPass != "" {
if !regexp.MustCompile("^[a-zA-Z0-9]+$").MatchString(args.publishPass) { if !regexp.MustCompile("^[a-zA-Z0-9]+$").MatchString(conf.PublishPass) {
return nil, fmt.Errorf("publish password must be alphanumeric") return nil, fmt.Errorf("publish password must be alphanumeric")
} }
} }
publishIps, err := parseIpCidrList(args.publishIps) publishIps, err := parseIpCidrList(conf.PublishIps)
if err != nil { if err != nil {
return nil, err 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") return nil, fmt.Errorf("read username and password must be both filled")
} }
if args.readUser != "" { if conf.ReadUser != "" {
if !regexp.MustCompile("^[a-zA-Z0-9]+$").MatchString(args.readUser) { if !regexp.MustCompile("^[a-zA-Z0-9]+$").MatchString(conf.ReadUser) {
return nil, fmt.Errorf("read username must be alphanumeric") return nil, fmt.Errorf("read username must be alphanumeric")
} }
} }
if args.readPass != "" { if conf.ReadPass != "" {
if !regexp.MustCompile("^[a-zA-Z0-9]+$").MatchString(args.readPass) { if !regexp.MustCompile("^[a-zA-Z0-9]+$").MatchString(conf.ReadPass) {
return nil, fmt.Errorf("read password must be alphanumeric") 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") 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 { if err != nil {
return nil, err return nil, err
} }
p := &program{ p := &program{
args: args, conf: conf,
protocols: protocols, protocols: protocols,
publishIps: publishIps, publishIps: publishIps,
readIps: readIps, readIps: readIps,
@ -327,7 +353,7 @@ func newProgram(sargs []string) (*program, error) {
p.log("rtsp-simple-server %s", Version) p.log("rtsp-simple-server %s", Version)
if args.pprof { if conf.Pprof {
go func(mux *http.ServeMux) { go func(mux *http.ServeMux) {
server := &http.Server{ server := &http.Server{
Addr: ":9999", Addr: ":9999",
@ -339,12 +365,12 @@ func newProgram(sargs []string) (*program, error) {
http.DefaultServeMux = http.NewServeMux() 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 { if err != nil {
return nil, err 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 { if err != nil {
return nil, err return nil, err
} }
@ -607,7 +633,7 @@ func (p *program) forwardTrack(path string, id int, trackFlowType trackFlowType,
} }
func main() { func main() {
_, err := newProgram(os.Args[1:]) _, err := newProgram(os.Args[1:], os.Stdin)
if err != nil { if err != nil {
log.Fatal("ERR: ", err) log.Fatal("ERR: ", err)
} }

22
main_test.go

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

22
server-client.go

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

4
server-tcpl.go

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

2
server-udpl.go

@ -62,7 +62,7 @@ func (l *serverUdpListener) log(format string, args ...interface{}) {
func (l *serverUdpListener) run() { func (l *serverUdpListener) run() {
go func() { go func() {
for w := range l.writec { 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) l.nconn.WriteTo(w.buf, w.addr)
} }
}() }()

Loading…
Cancel
Save