diff --git a/pkg/media/media_test.go b/pkg/media/media_test.go index 0436fe92..1c084bf6 100644 --- a/pkg/media/media_test.go +++ b/pkg/media/media_test.go @@ -61,7 +61,7 @@ func TestMediaURL(t *testing.T) { mustParseURL("rtsp://myuser:mypass@192.168.1.99:554/sub/path/trackID=5"), }, { - "relative control, url without slash", + "relative control, subpath, without slash", []byte("v=0\r\n" + "m=video 0 RTP/AVP 96\r\n" + "a=rtpmap:96 H264/90000\r\n" + diff --git a/pkg/url/url.go b/pkg/url/url.go index 2271c110..cc9d7a80 100644 --- a/pkg/url/url.go +++ b/pkg/url/url.go @@ -87,11 +87,5 @@ func (u *URL) RTSPPathAndQuery() (string, bool) { pathAndQuery += "?" + u.RawQuery } - // remove leading slash - if len(pathAndQuery) == 0 || pathAndQuery[0] != '/' { - return "", false - } - pathAndQuery = pathAndQuery[1:] - return pathAndQuery, true } diff --git a/pkg/url/url_test.go b/pkg/url/url_test.go index 4a49a1d4..7bc12985 100644 --- a/pkg/url/url_test.go +++ b/pkg/url/url_test.go @@ -113,28 +113,49 @@ func TestURLCloneWithoutCredentials(t *testing.T) { func TestURLRTSPPathAndQuery(t *testing.T) { for _, ca := range []struct { - u *URL - b string + name string + u *URL + b string }{ { + "standard", mustParse("rtsp://localhost:8554/teststream/trackID=1"), - "teststream/trackID=1", + "/teststream/trackID=1", }, { + "subpath", mustParse("rtsp://localhost:8554/test/stream/trackID=1"), - "test/stream/trackID=1", + "/test/stream/trackID=1", }, { + "path and query", mustParse("rtsp://192.168.1.99:554/test?user=tmp&password=BagRep1&channel=1&stream=0.sdp/trackID=1"), - "test?user=tmp&password=BagRep1&channel=1&stream=0.sdp/trackID=1", + "/test?user=tmp&password=BagRep1&channel=1&stream=0.sdp/trackID=1", }, { + "path and query with special chars", mustParse("rtsp://192.168.1.99:554/te!st?user=tmp&password=BagRep1!&channel=1&stream=0.sdp/trackID=1"), - "te!st?user=tmp&password=BagRep1!&channel=1&stream=0.sdp/trackID=1", + "/te!st?user=tmp&password=BagRep1!&channel=1&stream=0.sdp/trackID=1", }, { + "path and query attached", mustParse("rtsp://192.168.1.99:554/user=tmp&password=BagRep1!&channel=1&stream=0.sdp/trackID=1"), - "user=tmp&password=BagRep1!&channel=1&stream=0.sdp/trackID=1", + "/user=tmp&password=BagRep1!&channel=1&stream=0.sdp/trackID=1", + }, + { + "no path", + mustParse("rtsp://192.168.1.99:554"), + "", + }, + { + "single slash", + mustParse("rtsp://192.168.1.99:554/"), + "/", + }, + { + "no slash and query", + mustParse("rtsp://192.168.1.99:554?testing"), + "?testing", }, } { b, ok := ca.u.RTSPPathAndQuery() diff --git a/server_play_test.go b/server_play_test.go index 0e6ceb6e..a3d3eaf1 100644 --- a/server_play_test.go +++ b/server_play_test.go @@ -82,42 +82,61 @@ func doDescribe(conn *conn.Conn) (*sdp.SessionDescription, error) { return &desc, err } -func TestServerPlaySetupPath(t *testing.T) { +func TestServerPlayPath(t *testing.T) { for _, ca := range []struct { - name string - url string - path string + name string + setupURL string + playURL string + path string }{ { "normal", - "rtsp://localhost:8554/teststream/[control2]", - "teststream", + "rtsp://localhost:8554/teststream[control]", + "rtsp://localhost:8554/teststream/", + "/teststream", }, { "with query", - "rtsp://localhost:8554/teststream?testing=123/[control4]", - "teststream", + "rtsp://localhost:8554/teststream?testing=123[control]", + "rtsp://localhost:8554/teststream/", + "/teststream", }, { // this is needed to support reading mpegts with ffmpeg "without media id", "rtsp://localhost:8554/teststream/", - "teststream", + "rtsp://localhost:8554/teststream/", + "/teststream", }, { "subpath", - "rtsp://localhost:8554/test/stream/[control0]", - "test/stream", + "rtsp://localhost:8554/test/stream[control]", + "rtsp://localhost:8554/test/stream/", + "/test/stream", }, { "subpath without media id", "rtsp://localhost:8554/test/stream/", - "test/stream", + "rtsp://localhost:8554/test/stream/", + "/test/stream", }, { "subpath with query", - "rtsp://localhost:8554/test/stream?testing=123/[control4]", - "test/stream", + "rtsp://localhost:8554/test/stream?testing=123[control]", + "rtsp://localhost:8554/test/stream", + "/test/stream", + }, + { + "no slash", + "rtsp://localhost:8554[control]", + "rtsp://localhost:8554/", + "", + }, + { + "single slash", + "rtsp://localhost:8554/[control]", + "rtsp://localhost:8554//", + "/", }, } { t.Run(ca.name, func(t *testing.T) { @@ -126,7 +145,6 @@ func TestServerPlaySetupPath(t *testing.T) { testH264Media, testH264Media, testH264Media, - testH264Media, }) defer stream.Close() @@ -139,10 +157,18 @@ func TestServerPlaySetupPath(t *testing.T) { }, onSetup: func(ctx *ServerHandlerOnSetupCtx) (*base.Response, *ServerStream, error) { require.Equal(t, ca.path, ctx.Path) + return &base.Response{ StatusCode: base.StatusOK, }, stream, nil }, + onPlay: func(ctx *ServerHandlerOnPlayCtx) (*base.Response, error) { + require.Equal(t, ca.path, ctx.Path) + + return &base.Response{ + StatusCode: base.StatusOK, + }, nil + }, }, RTSPAddress: "localhost:8554", } @@ -172,14 +198,11 @@ func TestServerPlaySetupPath(t *testing.T) { InterleavedIDs: &[2]int{0, 1}, } - for i, md := range desc.MediaDescriptions { - v, _ := md.Attribute("control") - ca.url = strings.ReplaceAll(ca.url, "[control"+strconv.FormatInt(int64(i), 10)+"]", v) - } + v, _ := desc.MediaDescriptions[1].Attribute("control") res, err := writeReqReadRes(conn, base.Request{ Method: base.Setup, - URL: mustParseURL(ca.url), + URL: mustParseURL(strings.ReplaceAll(ca.setupURL, "[control]", "/"+v)), Header: base.Header{ "CSeq": base.HeaderValue{"2"}, "Transport": th.Marshal(), @@ -187,6 +210,21 @@ func TestServerPlaySetupPath(t *testing.T) { }) require.NoError(t, err) require.Equal(t, base.StatusOK, res.StatusCode) + + var sx headers.Session + err = sx.Unmarshal(res.Header["Session"]) + require.NoError(t, err) + + res, err = writeReqReadRes(conn, base.Request{ + Method: base.Play, + URL: mustParseURL(ca.playURL), + Header: base.Header{ + "CSeq": base.HeaderValue{"3"}, + "Session": base.HeaderValue{sx.Session}, + }, + }) + require.NoError(t, err) + require.Equal(t, base.StatusOK, res.StatusCode) }) } } diff --git a/server_record_test.go b/server_record_test.go index 12349f6a..d5a45495 100644 --- a/server_record_test.go +++ b/server_record_test.go @@ -100,15 +100,10 @@ func TestServerRecordErrorAnnounce(t *testing.T) { invalidURLAnnounceReq(t, "rtsp:// aaaaa"), "unable to generate media URL", }, - { - "invalid URL 2", - invalidURLAnnounceReq(t, "rtsp://host"), - "invalid media URL (rtsp://localhost:8554)", - }, { "invalid URL 3", invalidURLAnnounceReq(t, "rtsp://host/otherpath"), - "invalid media path: must begin with 'teststream', but is 'otherpath'", + "invalid media path: must begin with '/teststream', but is '/otherpath'", }, } { t.Run(ca.name, func(t *testing.T) { @@ -146,30 +141,54 @@ func TestServerRecordErrorAnnounce(t *testing.T) { } } -func TestServerRecordSetupPath(t *testing.T) { +func TestServerRecordPath(t *testing.T) { for _, ca := range []struct { - name string - control string - url string - path string + name string + control string + announceURL string + setupURL string + path string + query string }{ { "normal", "bbb=ccc", + "rtsp://localhost:8554/teststream", "rtsp://localhost:8554/teststream/bbb=ccc", - "teststream", + "/teststream", + "", }, { "subpath", "ddd=eee", + "rtsp://localhost:8554/test/stream", "rtsp://localhost:8554/test/stream/ddd=eee", - "test/stream", + "/test/stream", + "", }, { "subpath and query", "fff=ggg", + "rtsp://localhost:8554/test/stream?testing=0", "rtsp://localhost:8554/test/stream?testing=0/fff=ggg", - "test/stream?testing=0", + "/test/stream", + "testing=0", + }, + { + "no path", + "streamid=1", + "rtsp://localhost:8554", + "rtsp://localhost:8554/streamid=1", + "", + "", + }, + { + "single slash", + "streamid=1", + "rtsp://localhost:8554/", + "rtsp://localhost:8554//streamid=1", + "/", + "", }, } { t.Run(ca.name, func(t *testing.T) { @@ -185,15 +204,21 @@ func TestServerRecordSetupPath(t *testing.T) { }, nil }, onSetup: func(ctx *ServerHandlerOnSetupCtx) (*base.Response, *ServerStream, error) { - p := ctx.Path - if ctx.Query != "" { - p += "?" + ctx.Query - } - require.Equal(t, ca.path, p) + require.Equal(t, ca.path, ctx.Path) + require.Equal(t, ca.query, ctx.Query) + return &base.Response{ StatusCode: base.StatusOK, }, nil, nil }, + onRecord: func(ctx *ServerHandlerOnRecordCtx) (*base.Response, error) { + require.Equal(t, ca.path, ctx.Path) + require.Equal(t, ca.query, ctx.Query) + + return &base.Response{ + StatusCode: base.StatusOK, + }, nil + }, }, RTSPAddress: "localhost:8554", } @@ -230,7 +255,7 @@ func TestServerRecordSetupPath(t *testing.T) { res, err := writeReqReadRes(conn, base.Request{ Method: base.Announce, - URL: mustParseURL("rtsp://localhost:8554/" + ca.path), + URL: mustParseURL(ca.announceURL), Header: base.Header{ "CSeq": base.HeaderValue{"1"}, "Content-Type": base.HeaderValue{"application/sdp"}, @@ -255,7 +280,7 @@ func TestServerRecordSetupPath(t *testing.T) { res, err = writeReqReadRes(conn, base.Request{ Method: base.Setup, - URL: mustParseURL(ca.url), + URL: mustParseURL(ca.setupURL), Header: base.Header{ "CSeq": base.HeaderValue{"2"}, "Transport": th.Marshal(), @@ -263,6 +288,21 @@ func TestServerRecordSetupPath(t *testing.T) { }) require.NoError(t, err) require.Equal(t, base.StatusOK, res.StatusCode) + + var sx headers.Session + err = sx.Unmarshal(res.Header["Session"]) + require.NoError(t, err) + + res, err = writeReqReadRes(conn, base.Request{ + Method: base.Record, + URL: mustParseURL(ca.announceURL), + Header: base.Header{ + "CSeq": base.HeaderValue{"3"}, + "Session": base.HeaderValue{sx.Session}, + }, + }) + require.NoError(t, err) + require.Equal(t, base.StatusOK, res.StatusCode) }) } } diff --git a/server_test.go b/server_test.go index 2271c1ca..45f5fe5e 100644 --- a/server_test.go +++ b/server_test.go @@ -1114,100 +1114,6 @@ func TestServerSessionTeardown(t *testing.T) { require.Equal(t, base.StatusOK, res.StatusCode) } -func TestServerErrorInvalidPath(t *testing.T) { - for _, ca := range []string{"inside session", "outside session"} { - t.Run(ca, func(t *testing.T) { - nconnClosed := make(chan struct{}) - - stream := NewServerStream(media.Medias{testH264Media}) - defer stream.Close() - - s := &Server{ - Handler: &testServerHandler{ - onConnClose: func(ctx *ServerHandlerOnConnCloseCtx) { - require.EqualError(t, ctx.Error, "invalid path") - close(nconnClosed) - }, - onDescribe: func(ctx *ServerHandlerOnDescribeCtx) (*base.Response, *ServerStream, error) { - return &base.Response{ - StatusCode: base.StatusOK, - }, stream, nil - }, - onSetup: func(ctx *ServerHandlerOnSetupCtx) (*base.Response, *ServerStream, error) { - return &base.Response{ - StatusCode: base.StatusOK, - }, stream, nil - }, - }, - RTSPAddress: "localhost:8554", - } - - err := s.Start() - require.NoError(t, err) - defer s.Close() - - nconn, err := net.Dial("tcp", "localhost:8554") - require.NoError(t, err) - defer nconn.Close() - conn := conn.NewConn(nconn) - - desc, err := doDescribe(conn) - require.NoError(t, err) - - if ca == "inside session" { - res, err := writeReqReadRes(conn, base.Request{ - Method: base.Setup, - URL: mustParseURL("rtsp://localhost:8554/teststream/" + controlAttribute(desc.MediaDescriptions[0])), - Header: base.Header{ - "CSeq": base.HeaderValue{"1"}, - "Transport": headers.Transport{ - Protocol: headers.TransportProtocolTCP, - Delivery: func() *headers.TransportDelivery { - v := headers.TransportDeliveryUnicast - return &v - }(), - Mode: func() *headers.TransportMode { - v := headers.TransportModePlay - return &v - }(), - InterleavedIDs: &[2]int{0, 1}, - }.Marshal(), - }, - }) - require.NoError(t, err) - require.Equal(t, base.StatusOK, res.StatusCode) - - var sx headers.Session - err = sx.Unmarshal(res.Header["Session"]) - require.NoError(t, err) - - res, err = writeReqReadRes(conn, base.Request{ - Method: base.SetParameter, - URL: mustParseURL("rtsp://localhost:8554"), - Header: base.Header{ - "CSeq": base.HeaderValue{"2"}, - "Session": base.HeaderValue{sx.Session}, - }, - }) - require.NoError(t, err) - require.Equal(t, base.StatusBadRequest, res.StatusCode) - } else { - res, err := writeReqReadRes(conn, base.Request{ - Method: base.SetParameter, - URL: mustParseURL("rtsp://localhost:8554"), - Header: base.Header{ - "CSeq": base.HeaderValue{"1"}, - }, - }) - require.NoError(t, err) - require.Equal(t, base.StatusBadRequest, res.StatusCode) - } - - <-nconnClosed - }) - } -} - func TestServerAuth(t *testing.T) { authValidator := auth.NewValidator("myuser", "mypass", nil) diff --git a/serversession.go b/serversession.go index 0fe12b70..6d409d50 100644 --- a/serversession.go +++ b/serversession.go @@ -450,8 +450,8 @@ func (ss *ServerSession) handleRequest(sc *ServerConn, req *base.Request) (*base }, liberrors.ErrServerInvalidPath{} } - if req.Method != base.Announce { - // path can end with a slash due to Content-Base, remove it + // pathAndQuery can end with a slash due to Content-Base, remove it + if ss.state == ServerSessionStatePrePlay || ss.state == ServerSessionStatePlay { pathAndQuery = strings.TrimSuffix(pathAndQuery, "/") } @@ -694,12 +694,20 @@ func (ss *ServerSession) handleRequest(sc *ServerConn, req *base.Request) (*base case ServerSessionStateInitial, ServerSessionStatePrePlay: // play medi = findMediaByUUID(stream, mediaUUID) default: // record - medi = findMediaByURL(ss.announcedMedias, &url.URL{ + baseURL := &url.URL{ Scheme: req.URL.Scheme, Host: req.URL.Host, Path: path, RawQuery: query, - }, req.URL) + } + + if baseURL.RawQuery != "" { + baseURL.RawQuery += "/" + } else { + baseURL.Path += "/" + } + + medi = findMediaByURL(ss.announcedMedias, baseURL, req.URL) } if medi == nil { @@ -824,8 +832,7 @@ func (ss *ServerSession) handleRequest(sc *ServerConn, req *base.Request) (*base }, err } - if ss.State() == ServerSessionStatePrePlay && - path != *ss.setuppedPath { + if ss.State() == ServerSessionStatePrePlay && path != *ss.setuppedPath { return &base.Response{ StatusCode: base.StatusBadRequest, }, liberrors.ErrServerPathHasChanged{Prev: *ss.setuppedPath, Cur: path}