diff --git a/README.md b/README.md index 699b0015..99a44fc3 100644 --- a/README.md +++ b/README.md @@ -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 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 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: + [] path to a config file. The default is conf.yml. Use 'stdin' to + read config from stdin ``` #### Compile and run from source diff --git a/main.go b/main.go index bd5ea87a..7fce5b62 100644 --- a/main.go +++ b/main.go @@ -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{} 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 { 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) { 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) { 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) { 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, } func main() { - _, err := newProgram(os.Args[1:]) + _, err := newProgram(os.Args[1:], os.Stdin) if err != nil { log.Fatal("ERR: ", err) } diff --git a/main_test.go b/main_test.go index 8e936f88..17c4e5ac 100644 --- a/main_test.go +++ b/main_test.go @@ -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) { } 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) { } 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() diff --git a/server-client.go b/server-client.go index ba22f62f..f8c60b7b 100644 --- a/server-client.go +++ b/server-client.go @@ -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 { } 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() { }() 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 { 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 { 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 { 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 { "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 { "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"}, }, diff --git a/server-tcpl.go b/server-tcpl.go index 5d1a6fa3..34f59fc9 100644 --- a/server-tcpl.go +++ b/server-tcpl.go @@ -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) { done: make(chan struct{}), } - l.log("opened on :%d", p.args.rtspPort) + l.log("opened on :%d", p.conf.RtspPort) return l, nil } diff --git a/server-udpl.go b/server-udpl.go index cf135262..aa4af5df 100644 --- a/server-udpl.go +++ b/server-udpl.go @@ -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) } }()