diff --git a/README.md b/README.md index f41d66d3936..dcbe01413a5 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ _rtsp-simple-server_ is a simple, ready-to-use and zero-dependency RTSP server a Features: * Read and publish live streams with UDP and TCP -* Each stream can have multiple video and audio tracks, encoded with any codec (including H264, H265, VP8, VP9, MP3, AAC, Opus, PCM) +* Each stream can have multiple video and audio tracks, encoded with any codec (including H264, H265, VP8, VP9, MPEG2, MP3, AAC, Opus, PCM) * Serve multiple streams at once in separate paths * Encrypt streams with TLS (RTSPS) * Pull and serve streams from other RTSP or RTMP servers, always or on-demand (RTSP proxy) diff --git a/go.mod b/go.mod index dcaf14667f7..a8cac8519af 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.15 require ( github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d // indirect - github.com/aler9/gortsplib v0.0.0-20210110113516-c3805aadc400 + github.com/aler9/gortsplib v0.0.0-20210119134354-d9b819e85e9b github.com/davecgh/go-spew v1.1.1 // indirect github.com/fsnotify/fsnotify v1.4.9 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 diff --git a/go.sum b/go.sum index de5f99c1761..775fa31ebd2 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,8 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafo github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d h1:UQZhZ2O0vMHr2cI+DC1Mbh0TJxzA3RcLoMsFw+aXw7E= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= -github.com/aler9/gortsplib v0.0.0-20210110113516-c3805aadc400 h1:mRaK+tJuQQ39M6poSfluxSu94kdqfzMrIi24+zacWeQ= -github.com/aler9/gortsplib v0.0.0-20210110113516-c3805aadc400/go.mod h1:8P09VjpiPJFyfkVosyF5/TY82jNwkMN165NS/7sc32I= +github.com/aler9/gortsplib v0.0.0-20210119134354-d9b819e85e9b h1:gNkdUoWMsgM9URQIWPF8fUgShKiSxteG463yNWZbIiA= +github.com/aler9/gortsplib v0.0.0-20210119134354-d9b819e85e9b/go.mod h1:8P09VjpiPJFyfkVosyF5/TY82jNwkMN165NS/7sc32I= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= diff --git a/internal/client/client.go b/internal/client/client.go index 3afdf45af58..0730a32943f 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -158,16 +158,16 @@ func (c *Client) run() { } onDescribe := func(req *base.Request) (*base.Response, error) { - basePath, ok := req.URL.BasePath() + reqPath, ok := req.URL.RTSPPath() if !ok { return &base.Response{ StatusCode: base.StatusBadRequest, - }, fmt.Errorf("unable to find base path (%s)", req.URL) + }, fmt.Errorf("invalid path (%s)", req.URL) } c.describeData = make(chan describeData) - path, err := c.parent.OnClientDescribe(c, basePath, req) + path, err := c.parent.OnClientDescribe(c, reqPath, req) if err != nil { switch terr := err.(type) { case errAuthNotCritical: @@ -199,7 +199,7 @@ func (c *Client) run() { c.path = nil if res.err != nil { - c.log(logger.Info, "no one is publishing to path '%s'", basePath) + c.log(logger.Info, "no one is publishing to path '%s'", reqPath) return &base.Response{ StatusCode: base.StatusNotFound, }, nil @@ -242,14 +242,14 @@ func (c *Client) run() { } onAnnounce := func(req *base.Request, tracks gortsplib.Tracks) (*base.Response, error) { - basePath, ok := req.URL.BasePath() + reqPath, ok := req.URL.RTSPPath() if !ok { return &base.Response{ StatusCode: base.StatusBadRequest, - }, fmt.Errorf("unable to find base path (%s)", req.URL) + }, fmt.Errorf("invalid path (%s)", req.URL) } - path, err := c.parent.OnClientAnnounce(c, basePath, tracks, req) + path, err := c.parent.OnClientAnnounce(c, reqPath, tracks, req) if err != nil { switch terr := err.(type) { case errAuthNotCritical: @@ -281,19 +281,28 @@ func (c *Client) run() { } onSetup := func(req *base.Request, th *headers.Transport, trackID int) (*base.Response, error) { - basePath, _, ok := req.URL.BasePathControlAttr() - if !ok { - return &base.Response{ - StatusCode: base.StatusBadRequest, - }, fmt.Errorf("unable to find control attribute (%s)", req.URL) - } - switch c.conn.State() { case gortsplib.ServerConnStateInitial, gortsplib.ServerConnStatePrePlay: // play - if c.path != nil && basePath != c.path.Name() { + pathAndQuery, ok := req.URL.RTSPPathAndQuery() + if !ok { return &base.Response{ StatusCode: base.StatusBadRequest, - }, fmt.Errorf("path has changed, was '%s', now is '%s'", c.path.Name(), basePath) + }, fmt.Errorf("invalid path (%s)", req.URL) + } + + _, pathAndQuery, ok = base.PathSplitControlAttribute(pathAndQuery) + if !ok { + return &base.Response{ + StatusCode: base.StatusBadRequest, + }, fmt.Errorf("invalid path (%s)", req.URL) + } + + reqPath, _ := base.PathSplitQuery(pathAndQuery) + + if c.path != nil && reqPath != c.path.Name() { + return &base.Response{ + StatusCode: base.StatusBadRequest, + }, fmt.Errorf("path has changed, was '%s', now is '%s'", c.path.Name(), reqPath) } // play with UDP @@ -304,7 +313,7 @@ func (c *Client) run() { }, nil } - path, err := c.parent.OnClientSetupPlay(c, basePath, trackID, req) + path, err := c.parent.OnClientSetupPlay(c, reqPath, trackID, req) if err != nil { switch terr := err.(type) { case errAuthNotCritical: @@ -346,7 +355,7 @@ func (c *Client) run() { }, nil } - path, err := c.parent.OnClientSetupPlay(c, basePath, trackID, req) + path, err := c.parent.OnClientSetupPlay(c, reqPath, trackID, req) if err != nil { switch terr := err.(type) { case errAuthNotCritical: @@ -380,11 +389,18 @@ func (c *Client) run() { }, nil default: // record - // after ANNOUNCE, c.path is already set - if basePath != c.path.Name() { + reqPathAndQuery, ok := req.URL.RTSPPathAndQuery() + if !ok { return &base.Response{ StatusCode: base.StatusBadRequest, - }, fmt.Errorf("path has changed, was '%s', now is '%s'", c.path.Name(), basePath) + }, fmt.Errorf("invalid path (%s)", req.URL) + } + + if !strings.HasPrefix(reqPathAndQuery, c.path.Name()) { + return &base.Response{ + StatusCode: base.StatusBadRequest, + }, fmt.Errorf("invalid path: must begin with '%s', but is '%s'", + c.path.Name(), reqPathAndQuery) } // record with UDP @@ -395,18 +411,6 @@ func (c *Client) run() { }, nil } - if th.ClientPorts == nil { - return &base.Response{ - StatusCode: base.StatusBadRequest, - }, fmt.Errorf("transport header does not have valid client ports (%s)", req.Header["Transport"]) - } - - if c.conn.TracksLen() >= c.path.SourceTrackCount() { - return &base.Response{ - StatusCode: base.StatusBadRequest, - }, fmt.Errorf("all the tracks have already been setup") - } - return &base.Response{ StatusCode: base.StatusOK, Header: base.Header{ @@ -423,12 +427,6 @@ func (c *Client) run() { }, nil } - if c.conn.TracksLen() >= c.path.SourceTrackCount() { - return &base.Response{ - StatusCode: base.StatusBadRequest, - }, fmt.Errorf("all the tracks have already been setup") - } - return &base.Response{ StatusCode: base.StatusOK, Header: base.Header{ @@ -440,20 +438,20 @@ func (c *Client) run() { onPlay := func(req *base.Request) (*base.Response, error) { if c.conn.State() == gortsplib.ServerConnStatePrePlay { - basePath, ok := req.URL.BasePath() + reqPath, ok := req.URL.RTSPPath() if !ok { return &base.Response{ StatusCode: base.StatusBadRequest, - }, fmt.Errorf("unable to find base path (%s)", req.URL) + }, fmt.Errorf("invalid path (%s)", req.URL) } // path can end with a slash, remove it - basePath = strings.TrimSuffix(basePath, "/") + reqPath = strings.TrimSuffix(reqPath, "/") - if basePath != c.path.Name() { + if reqPath != c.path.Name() { return &base.Response{ StatusCode: base.StatusBadRequest, - }, fmt.Errorf("path has changed, was '%s', now is '%s'", c.path.Name(), basePath) + }, fmt.Errorf("path has changed, was '%s', now is '%s'", c.path.Name(), reqPath) } c.startPlay() @@ -468,20 +466,20 @@ func (c *Client) run() { } onRecord := func(req *base.Request) (*base.Response, error) { - basePath, ok := req.URL.BasePath() + reqPath, ok := req.URL.RTSPPath() if !ok { return &base.Response{ StatusCode: base.StatusBadRequest, - }, fmt.Errorf("unable to find base path (%s)", req.URL) + }, fmt.Errorf("invalid path (%s)", req.URL) } // path can end with a slash, remove it - basePath = strings.TrimSuffix(basePath, "/") + reqPath = strings.TrimSuffix(reqPath, "/") - if basePath != c.path.Name() { + if reqPath != c.path.Name() { return &base.Response{ StatusCode: base.StatusBadRequest, - }, fmt.Errorf("path has changed, was '%s', now is '%s'", c.path.Name(), basePath) + }, fmt.Errorf("path has changed, was '%s', now is '%s'", c.path.Name(), reqPath) } c.startRecord() @@ -592,7 +590,8 @@ func (errAuthCritical) Error() string { } // Authenticate performs an authentication. -func (c *Client) Authenticate(authMethods []headers.AuthMethod, ips []interface{}, user string, pass string, req *base.Request) error { +func (c *Client) Authenticate(authMethods []headers.AuthMethod, ips []interface{}, + user string, pass string, req *base.Request, altURL *base.URL) error { // validate ip if ips != nil { ip := c.ip() @@ -615,7 +614,8 @@ func (c *Client) Authenticate(authMethods []headers.AuthMethod, ips []interface{ c.authValidator = auth.NewValidator(user, pass, authMethods) } - err := c.authValidator.ValidateHeader(req.Header["Authorization"], req.Method, req.URL) + err := c.authValidator.ValidateHeader(req.Header["Authorization"], + req.Method, req.URL, altURL) if err != nil { c.authFailures++ @@ -658,12 +658,15 @@ func (c *Client) Authenticate(authMethods []headers.AuthMethod, ips []interface{ func (c *Client) startPlay() { c.path.OnClientPlay(c) - c.log(logger.Info, "is reading from path '%s', %d %s with %s", c.path.Name(), c.conn.TracksLen(), func() string { - if c.conn.TracksLen() == 1 { - return "track" - } - return "tracks" - }(), *c.conn.TracksProtocol()) + c.log(logger.Info, "is reading from path '%s', %d %s with %s", c.path.Name(), + c.conn.SetuppedTracksLen(), + func() string { + if c.conn.SetuppedTracksLen() == 1 { + return "track" + } + return "tracks" + }(), + *c.conn.SetuppedTracksProtocol()) if c.path.Conf().RunOnRead != "" { c.onReadCmd = externalcmd.New(c.path.Conf().RunOnRead, c.path.Conf().RunOnReadRestart, externalcmd.Environment{ @@ -682,12 +685,15 @@ func (c *Client) stopPlay() { func (c *Client) startRecord() { c.path.OnClientRecord(c) - c.log(logger.Info, "is publishing to path '%s', %d %s with %s", c.path.Name(), c.conn.TracksLen(), func() string { - if c.conn.TracksLen() == 1 { - return "track" - } - return "tracks" - }(), *c.conn.TracksProtocol()) + c.log(logger.Info, "is publishing to path '%s', %d %s with %s", c.path.Name(), + c.conn.SetuppedTracksLen(), + func() string { + if c.conn.SetuppedTracksLen() == 1 { + return "track" + } + return "tracks" + }(), + *c.conn.SetuppedTracksProtocol()) if c.path.Conf().RunOnPublish != "" { c.onPublishCmd = externalcmd.New(c.path.Conf().RunOnPublish, c.path.Conf().RunOnPublishRestart, externalcmd.Environment{ @@ -705,7 +711,7 @@ func (c *Client) stopRecord() { // OnReaderFrame implements path.Reader. func (c *Client) OnReaderFrame(trackID int, streamType base.StreamType, buf []byte) { - if !c.conn.HasTrack(trackID) { + if !c.conn.HasSetuppedTrack(trackID) { return } diff --git a/internal/pathman/pathman.go b/internal/pathman/pathman.go index db392ea0472..3af8bc00c6e 100644 --- a/internal/pathman/pathman.go +++ b/internal/pathman/pathman.go @@ -155,7 +155,7 @@ outer: } err = req.Client.Authenticate(pm.authMethods, pathConf.ReadIpsParsed, - pathConf.ReadUser, pathConf.ReadPass, req.Req) + pathConf.ReadUser, pathConf.ReadPass, req.Req, nil) if err != nil { req.Res <- path.ClientDescribeRes{nil, err} //nolint:govet continue @@ -187,7 +187,8 @@ outer: } err = req.Client.Authenticate(pm.authMethods, - pathConf.PublishIpsParsed, pathConf.PublishUser, pathConf.PublishPass, req.Req) + pathConf.PublishIpsParsed, pathConf.PublishUser, + pathConf.PublishPass, req.Req, nil) if err != nil { req.Res <- path.ClientAnnounceRes{nil, err} //nolint:govet continue @@ -223,8 +224,17 @@ outer: continue } + // VLC strips the control attribute + // provide an alternative URL without the control attribute + altURL := &base.URL{ + Scheme: req.Req.URL.Scheme, + Host: req.Req.URL.Host, + Path: "/" + req.PathName + "/", + } + err = req.Client.Authenticate(pm.authMethods, - pathConf.ReadIpsParsed, pathConf.ReadUser, pathConf.ReadPass, req.Req) + pathConf.ReadIpsParsed, pathConf.ReadUser, pathConf.ReadPass, + req.Req, altURL) if err != nil { req.Res <- path.ClientSetupPlayRes{nil, err} //nolint:govet continue