diff --git a/internal/servers/rtsp/conn.go b/internal/servers/rtsp/conn.go index 206db18e..374c5ae0 100644 --- a/internal/servers/rtsp/conn.go +++ b/internal/servers/rtsp/conn.go @@ -42,6 +42,15 @@ func credentialsProvided(req *base.Request) bool { return err == nil && auth.Username != "" } +func contains(list []rtspauth.VerifyMethod, item rtspauth.VerifyMethod) bool { + for _, i := range list { + if i == item { + return true + } + } + return false +} + type connParent interface { logger.Writer findSessionByRSessionUnsafe(rsession *gortsplib.ServerSession) *session @@ -138,16 +147,23 @@ func (c *conn) onDescribe(ctx *gortsplib.ServerHandlerOnDescribeCtx, } ctx.Path = ctx.Path[1:] - req := defs.PathAccessRequest{ - Name: ctx.Path, - Query: ctx.Query, - Proto: auth.ProtocolRTSP, - ID: &c.uuid, - Credentials: rtsp.Credentials(ctx.Request), - IP: c.ip(), - CustomVerifyFunc: func(expectedUser, expectedPass string) bool { + // CustomVerifyFunc prevents hashed credentials from working. + // Use it only when strictly needed. + var customVerifyFunc func(expectedUser, expectedPass string) bool + if contains(c.authMethods, rtspauth.VerifyMethodDigestMD5) { + customVerifyFunc = func(expectedUser, expectedPass string) bool { return c.rconn.VerifyCredentials(ctx.Request, expectedUser, expectedPass) - }, + } + } + + req := defs.PathAccessRequest{ + Name: ctx.Path, + Query: ctx.Query, + Proto: auth.ProtocolRTSP, + ID: &c.uuid, + Credentials: rtsp.Credentials(ctx.Request), + IP: c.ip(), + CustomVerifyFunc: customVerifyFunc, } res := c.pathManager.Describe(defs.PathDescribeReq{ diff --git a/internal/servers/rtsp/server_test.go b/internal/servers/rtsp/server_test.go index 1fe770a8..c7e12c6d 100644 --- a/internal/servers/rtsp/server_test.go +++ b/internal/servers/rtsp/server_test.go @@ -64,192 +64,249 @@ func (p *dummyPath) RemoveReader(_ defs.PathRemoveReaderReq) { } func TestServerPublish(t *testing.T) { - path := &dummyPath{ - streamCreated: make(chan struct{}), - } - - pathManager := &test.PathManager{ - AddPublisherImpl: func(req defs.PathAddPublisherReq) (defs.Path, error) { - if req.AccessRequest.Credentials.User == "" && req.AccessRequest.Credentials.Pass == "" { - return nil, auth.Error{Message: "", AskCredentials: true} + for _, ca := range []string{"basic", "digest", "basic+digest"} { + t.Run(ca, func(t *testing.T) { + path := &dummyPath{ + streamCreated: make(chan struct{}), } - require.Equal(t, "teststream", req.AccessRequest.Name) - require.Equal(t, "param=value", req.AccessRequest.Query) - require.Equal(t, "myuser", req.AccessRequest.Credentials.User) - require.Equal(t, "mypass", req.AccessRequest.Credentials.Pass) - return path, nil - }, - } - s := &Server{ - Address: "127.0.0.1:8557", - AuthMethods: []rtspauth.VerifyMethod{rtspauth.VerifyMethodBasic}, - ReadTimeout: conf.Duration(10 * time.Second), - WriteTimeout: conf.Duration(10 * time.Second), - WriteQueueSize: 512, - Transports: conf.RTSPTransports{gortsplib.TransportTCP: {}}, - PathManager: pathManager, - Parent: test.NilLogger, - } - err := s.Initialize() - require.NoError(t, err) - defer s.Close() + n := 0 - source := gortsplib.Client{} + pathManager := &test.PathManager{ + AddPublisherImpl: func(req defs.PathAddPublisherReq) (defs.Path, error) { + if ca == "basic" { + if req.AccessRequest.Credentials.User == "" && req.AccessRequest.Credentials.Pass == "" { + return nil, auth.Error{Message: "", AskCredentials: true} + } + require.Equal(t, "teststream", req.AccessRequest.Name) + require.Equal(t, "param=value", req.AccessRequest.Query) + require.Equal(t, "myuser", req.AccessRequest.Credentials.User) + require.Equal(t, "mypass", req.AccessRequest.Credentials.Pass) + } else { + ok := req.AccessRequest.CustomVerifyFunc("myuser", "mypass") + if n == 0 { + require.False(t, ok) + n++ + return nil, auth.Error{Message: "", AskCredentials: true} + } + require.True(t, ok) + } + return path, nil + }, + } - media0 := test.UniqueMediaH264() + var authMethods []rtspauth.VerifyMethod + switch ca { + case "basic": + authMethods = []rtspauth.VerifyMethod{rtspauth.VerifyMethodBasic} + case "digest": + authMethods = []rtspauth.VerifyMethod{rtspauth.VerifyMethodDigestMD5} + default: + authMethods = []rtspauth.VerifyMethod{rtspauth.VerifyMethodBasic, rtspauth.VerifyMethodDigestMD5} + } - err = source.StartRecording( - "rtsp://myuser:mypass@127.0.0.1:8557/teststream?param=value", - &description.Session{Medias: []*description.Media{media0}}) - require.NoError(t, err) - defer source.Close() + s := &Server{ + Address: "127.0.0.1:8557", + AuthMethods: authMethods, + ReadTimeout: conf.Duration(10 * time.Second), + WriteTimeout: conf.Duration(10 * time.Second), + WriteQueueSize: 512, + Transports: conf.RTSPTransports{gortsplib.TransportTCP: {}}, + PathManager: pathManager, + Parent: test.NilLogger, + } + err := s.Initialize() + require.NoError(t, err) + defer s.Close() - <-path.streamCreated + source := gortsplib.Client{} - reader := test.NilLogger + media0 := test.UniqueMediaH264() - recv := make(chan struct{}) + err = source.StartRecording( + "rtsp://myuser:mypass@127.0.0.1:8557/teststream?param=value", + &description.Session{Medias: []*description.Media{media0}}) + require.NoError(t, err) + defer source.Close() - path.stream.AddReader( - reader, - path.stream.Desc.Medias[0], - path.stream.Desc.Medias[0].Formats[0], - func(u unit.Unit) error { - require.Equal(t, [][]byte{ - test.FormatH264.SPS, - test.FormatH264.PPS, - {5, 2, 3, 4}, - }, u.(*unit.H264).AU) - close(recv) - return nil + <-path.streamCreated + + reader := test.NilLogger + + recv := make(chan struct{}) + + path.stream.AddReader( + reader, + path.stream.Desc.Medias[0], + path.stream.Desc.Medias[0].Formats[0], + func(u unit.Unit) error { + require.Equal(t, [][]byte{ + test.FormatH264.SPS, + test.FormatH264.PPS, + {5, 2, 3, 4}, + }, u.(*unit.H264).AU) + close(recv) + return nil + }) + + path.stream.StartReader(reader) + defer path.stream.RemoveReader(reader) + + err = source.WritePacketRTP(media0, &rtp.Packet{ + Header: rtp.Header{ + Version: 2, + Marker: true, + PayloadType: 96, + SequenceNumber: 123, + Timestamp: 45343, + SSRC: 563423, + }, + Payload: []byte{5, 2, 3, 4}, + }) + require.NoError(t, err) + + <-recv }) - - path.stream.StartReader(reader) - defer path.stream.RemoveReader(reader) - - err = source.WritePacketRTP(media0, &rtp.Packet{ - Header: rtp.Header{ - Version: 2, - Marker: true, - PayloadType: 96, - SequenceNumber: 123, - Timestamp: 45343, - SSRC: 563423, - }, - Payload: []byte{5, 2, 3, 4}, - }) - require.NoError(t, err) - - <-recv + } } func TestServerRead(t *testing.T) { - desc := &description.Session{Medias: []*description.Media{test.MediaH264}} + for _, ca := range []string{"basic", "digest", "basic+digest"} { + t.Run(ca, func(t *testing.T) { + desc := &description.Session{Medias: []*description.Media{test.MediaH264}} - strm := &stream.Stream{ - WriteQueueSize: 512, - UDPMaxPayloadSize: 1472, - Desc: desc, - GenerateRTPPackets: true, - Parent: test.NilLogger, - } - err := strm.Initialize() - require.NoError(t, err) - - path := &dummyPath{stream: strm} - - pathManager := &test.PathManager{ - DescribeImpl: func(req defs.PathDescribeReq) defs.PathDescribeRes { - if req.AccessRequest.Credentials.User == "" && req.AccessRequest.Credentials.Pass == "" { - return defs.PathDescribeRes{Err: auth.Error{Message: "", AskCredentials: true}} + strm := &stream.Stream{ + WriteQueueSize: 512, + UDPMaxPayloadSize: 1472, + Desc: desc, + GenerateRTPPackets: true, + Parent: test.NilLogger, } - require.Equal(t, "teststream", req.AccessRequest.Name) - require.Equal(t, "param=value", req.AccessRequest.Query) - require.Equal(t, "myuser", req.AccessRequest.Credentials.User) - require.Equal(t, "mypass", req.AccessRequest.Credentials.Pass) + err := strm.Initialize() + require.NoError(t, err) - return defs.PathDescribeRes{ - Path: path, - Stream: path.stream, - Err: nil, + path := &dummyPath{stream: strm} + n := 0 + + pathManager := &test.PathManager{ + DescribeImpl: func(req defs.PathDescribeReq) defs.PathDescribeRes { + if ca == "basic" { + if req.AccessRequest.Credentials.User == "" && req.AccessRequest.Credentials.Pass == "" { + return defs.PathDescribeRes{Err: auth.Error{Message: "", AskCredentials: true}} + } + require.Equal(t, "teststream", req.AccessRequest.Name) + require.Equal(t, "param=value", req.AccessRequest.Query) + require.Equal(t, "myuser", req.AccessRequest.Credentials.User) + require.Equal(t, "mypass", req.AccessRequest.Credentials.Pass) + } else { + ok := req.AccessRequest.CustomVerifyFunc("myuser", "mypass") + if n == 0 { + require.False(t, ok) + n++ + return defs.PathDescribeRes{Err: auth.Error{Message: "", AskCredentials: true}} + } + require.True(t, ok) + } + + return defs.PathDescribeRes{ + Path: path, + Stream: path.stream, + Err: nil, + } + }, + AddReaderImpl: func(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) { + if ca == "basic" { + require.Equal(t, "teststream", req.AccessRequest.Name) + require.Equal(t, "param=value", req.AccessRequest.Query) + require.Equal(t, "myuser", req.AccessRequest.Credentials.User) + require.Equal(t, "mypass", req.AccessRequest.Credentials.Pass) + } else { + ok := req.AccessRequest.CustomVerifyFunc("myuser", "mypass") + require.True(t, ok) + } + + return path, path.stream, nil + }, } - }, - AddReaderImpl: func(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) { - require.Equal(t, "teststream", req.AccessRequest.Name) - require.Equal(t, "param=value", req.AccessRequest.Query) - require.Equal(t, "myuser", req.AccessRequest.Credentials.User) - require.Equal(t, "mypass", req.AccessRequest.Credentials.Pass) - return path, path.stream, nil - }, + + var authMethods []rtspauth.VerifyMethod + switch ca { + case "basic": + authMethods = []rtspauth.VerifyMethod{rtspauth.VerifyMethodBasic} + case "digest": + authMethods = []rtspauth.VerifyMethod{rtspauth.VerifyMethodDigestMD5} + default: + authMethods = []rtspauth.VerifyMethod{rtspauth.VerifyMethodBasic, rtspauth.VerifyMethodDigestMD5} + } + + s := &Server{ + Address: "127.0.0.1:8557", + AuthMethods: authMethods, + ReadTimeout: conf.Duration(10 * time.Second), + WriteTimeout: conf.Duration(10 * time.Second), + WriteQueueSize: 512, + Transports: conf.RTSPTransports{gortsplib.TransportTCP: {}}, + PathManager: pathManager, + Parent: test.NilLogger, + } + err = s.Initialize() + require.NoError(t, err) + defer s.Close() + + reader := gortsplib.Client{} + + u, err := base.ParseURL("rtsp://myuser:mypass@127.0.0.1:8557/teststream?param=value") + require.NoError(t, err) + + err = reader.Start(u.Scheme, u.Host) + require.NoError(t, err) + defer reader.Close() + + desc2, _, err := reader.Describe(u) + require.NoError(t, err) + + err = reader.SetupAll(desc2.BaseURL, desc2.Medias) + require.NoError(t, err) + + recv := make(chan struct{}) + + reader.OnPacketRTPAny(func(_ *description.Media, _ format.Format, p *rtp.Packet) { + require.Equal(t, &rtp.Packet{ + Header: rtp.Header{ + Version: 2, + Marker: true, + PayloadType: 96, + SequenceNumber: p.SequenceNumber, + Timestamp: p.Timestamp, + SSRC: p.SSRC, + CSRC: []uint32{}, + }, + Payload: []byte{ + 0x18, 0x00, 0x19, 0x67, 0x42, 0xc0, 0x28, 0xd9, + 0x00, 0x78, 0x02, 0x27, 0xe5, 0x84, 0x00, 0x00, + 0x03, 0x00, 0x04, 0x00, 0x00, 0x03, 0x00, 0xf0, + 0x3c, 0x60, 0xc9, 0x20, 0x00, 0x04, 0x08, 0x06, + 0x07, 0x08, 0x00, 0x04, 0x05, 0x02, 0x03, 0x04, + }, + }, p) + close(recv) + }) + + _, err = reader.Play(nil) + require.NoError(t, err) + + strm.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.H264{ + Base: unit.Base{ + NTP: time.Time{}, + }, + AU: [][]byte{ + {5, 2, 3, 4}, // IDR + }, + }) + + <-recv + }) } - - s := &Server{ - Address: "127.0.0.1:8557", - AuthMethods: []rtspauth.VerifyMethod{rtspauth.VerifyMethodBasic}, - ReadTimeout: conf.Duration(10 * time.Second), - WriteTimeout: conf.Duration(10 * time.Second), - WriteQueueSize: 512, - Transports: conf.RTSPTransports{gortsplib.TransportTCP: {}}, - PathManager: pathManager, - Parent: test.NilLogger, - } - err = s.Initialize() - require.NoError(t, err) - defer s.Close() - - reader := gortsplib.Client{} - - u, err := base.ParseURL("rtsp://myuser:mypass@127.0.0.1:8557/teststream?param=value") - require.NoError(t, err) - - err = reader.Start(u.Scheme, u.Host) - require.NoError(t, err) - defer reader.Close() - - desc2, _, err := reader.Describe(u) - require.NoError(t, err) - - err = reader.SetupAll(desc2.BaseURL, desc2.Medias) - require.NoError(t, err) - - recv := make(chan struct{}) - - reader.OnPacketRTPAny(func(_ *description.Media, _ format.Format, p *rtp.Packet) { - require.Equal(t, &rtp.Packet{ - Header: rtp.Header{ - Version: 2, - Marker: true, - PayloadType: 96, - SequenceNumber: p.SequenceNumber, - Timestamp: p.Timestamp, - SSRC: p.SSRC, - CSRC: []uint32{}, - }, - Payload: []byte{ - 0x18, 0x00, 0x19, 0x67, 0x42, 0xc0, 0x28, 0xd9, - 0x00, 0x78, 0x02, 0x27, 0xe5, 0x84, 0x00, 0x00, - 0x03, 0x00, 0x04, 0x00, 0x00, 0x03, 0x00, 0xf0, - 0x3c, 0x60, 0xc9, 0x20, 0x00, 0x04, 0x08, 0x06, - 0x07, 0x08, 0x00, 0x04, 0x05, 0x02, 0x03, 0x04, - }, - }, p) - close(recv) - }) - - _, err = reader.Play(nil) - require.NoError(t, err) - - strm.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.H264{ - Base: unit.Base{ - NTP: time.Time{}, - }, - AU: [][]byte{ - {5, 2, 3, 4}, // IDR - }, - }) - - <-recv } func TestServerRedirect(t *testing.T) { diff --git a/internal/servers/rtsp/session.go b/internal/servers/rtsp/session.go index 28236cf0..dadfa5f4 100644 --- a/internal/servers/rtsp/session.go +++ b/internal/servers/rtsp/session.go @@ -9,6 +9,7 @@ import ( "time" "github.com/bluenviron/gortsplib/v4" + rtspauth "github.com/bluenviron/gortsplib/v4/pkg/auth" "github.com/bluenviron/gortsplib/v4/pkg/base" "github.com/google/uuid" @@ -144,17 +145,24 @@ func (s *session) onAnnounce(c *conn, ctx *gortsplib.ServerHandlerOnAnnounceCtx) } ctx.Path = ctx.Path[1:] - req := defs.PathAccessRequest{ - Name: ctx.Path, - Query: ctx.Query, - Publish: true, - Proto: auth.ProtocolRTSP, - ID: &c.uuid, - Credentials: rtsp.Credentials(ctx.Request), - IP: c.ip(), - CustomVerifyFunc: func(expectedUser, expectedPass string) bool { + // CustomVerifyFunc prevents hashed credentials from working. + // Use it only when strictly needed. + var customVerifyFunc func(expectedUser, expectedPass string) bool + if contains(c.authMethods, rtspauth.VerifyMethodDigestMD5) { + customVerifyFunc = func(expectedUser, expectedPass string) bool { return c.rconn.VerifyCredentials(ctx.Request, expectedUser, expectedPass) - }, + } + } + + req := defs.PathAccessRequest{ + Name: ctx.Path, + Query: ctx.Query, + Publish: true, + Proto: auth.ProtocolRTSP, + ID: &c.uuid, + Credentials: rtsp.Credentials(ctx.Request), + IP: c.ip(), + CustomVerifyFunc: customVerifyFunc, } path, err := s.pathManager.AddPublisher(defs.PathAddPublisherReq{