use AVP profile when scheme is RTSPS and protocol is TCP (#916)

plus, allow to pick between AVP and SAVP when scheme is RTSPS and protocol is TCP.

---------

Co-authored-by: aler9 <46489434+aler9@users.noreply.github.com>
This commit is contained in:
ProtoTess
2025-10-21 12:46:24 -04:00
committed by GitHub
parent cb169d3924
commit b4355d3099
2 changed files with 329 additions and 2 deletions

View File

@@ -1509,12 +1509,28 @@ func (c *Client) doAnnounce(u *base.URL, desc *description.Session) (*base.Respo
return nil, err
}
announceData, err := generateAnnounceData(desc, c.Scheme == "rtsps")
// Determine secure flag: TCP+RTSPS depends on media profile, others depend on scheme
var secure bool
// Determine secure flag: TCP+RTSPS depends on media profile, others depend on scheme
if c.Protocol != nil && *c.Protocol == ProtocolTCP && c.Scheme == "rtsps" {
// Check for all medias: if any media uses a secure profile, then secure is true
for _, medi := range desc.Medias {
if isSecure(medi.Profile) {
secure = true
break
}
}
} else {
secure = c.Scheme == "rtsps"
}
announceData, err := generateAnnounceData(desc, secure)
if err != nil {
return nil, err
}
err = prepareForAnnounce(desc, announceData, c.Scheme == "rtsps")
err = prepareForAnnounce(desc, announceData, secure)
if err != nil {
return nil, err
}

View File

@@ -0,0 +1,311 @@
package gortsplib
import (
"bufio"
"crypto/tls"
"net"
"strings"
"testing"
"github.com/stretchr/testify/require"
"github.com/bluenviron/gortsplib/v5/pkg/base"
"github.com/bluenviron/gortsplib/v5/pkg/conn"
"github.com/bluenviron/gortsplib/v5/pkg/description"
"github.com/bluenviron/gortsplib/v5/pkg/format"
"github.com/bluenviron/gortsplib/v5/pkg/headers"
)
// handleServerConnection handles the common server-side connection logic for the security profile tests
func handleServerConnection(t *testing.T, serverDone chan struct{}, nconn net.Conn) {
defer close(serverDone)
if nconn == nil {
return
}
defer nconn.Close()
conn := conn.NewConn(bufio.NewReader(nconn), nconn)
req, err2 := conn.ReadRequest()
require.NoError(t, err2)
require.Equal(t, base.Options, req.Method)
err2 = conn.WriteResponse(&base.Response{
StatusCode: base.StatusOK,
Header: base.Header{
"Public": base.HeaderValue{strings.Join([]string{
string(base.Announce),
string(base.Setup),
string(base.Record),
}, ", ")},
},
})
require.NoError(t, err2)
req, err2 = conn.ReadRequest()
require.NoError(t, err2)
require.Equal(t, base.Announce, req.Method)
err2 = conn.WriteResponse(&base.Response{
StatusCode: base.StatusOK,
})
require.NoError(t, err2)
}
// createSecureMedia creates a media description with secure profile (SAVP)
func createSecureMedia() *description.Media {
return &description.Media{
Type: description.MediaTypeVideo,
Profile: headers.TransportProfileSAVP, // This is the secure profile
Formats: []format.Format{&format.H264{
PayloadTyp: 96,
SPS: []byte{
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,
},
PPS: []byte{
0x44, 0x01, 0xc0, 0x25, 0x2f, 0x05, 0x32, 0x40,
},
PacketizationMode: 1,
}},
ID: "1",
Control: "trackID=0",
}
}
// createNonSecureMedia creates a media description with non-secure profile (RTP/AVP)
func createNonSecureMedia() *description.Media {
return &description.Media{
Type: description.MediaTypeVideo,
// Profile defaults to RTP/AVP which is not secure
Formats: []format.Format{&format.H264{
PayloadTyp: 96,
SPS: []byte{
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,
},
PPS: []byte{
0x44, 0x01, 0xc0, 0x25, 0x2f, 0x05, 0x32, 0x40,
},
PacketizationMode: 1,
}},
ID: "1",
Control: "trackID=0",
}
}
// createAudioMedia creates a non-secure audio media description
func createAudioMedia() *description.Media {
return &description.Media{
Type: description.MediaTypeAudio,
// Profile defaults to RTP/AVP which is not secure
Formats: []format.Format{&format.G711{
PayloadTyp: 0,
SampleRate: 8000,
ChannelCount: 1,
}},
ID: "2",
Control: "trackID=1",
}
}
// setupTLSTestServer creates a TLS listener and server goroutine for RTSPS testing
func setupTLSTestServer(t *testing.T) (net.Listener, chan struct{}) {
// Create TLS listener for RTSPS
cert, err := tls.X509KeyPair(serverCert, serverKey)
require.NoError(t, err)
l, err := tls.Listen("tcp", "localhost:0", &tls.Config{Certificates: []tls.Certificate{cert}})
require.NoError(t, err)
serverDone := make(chan struct{})
go func() {
nconn, err2 := l.Accept()
require.NoError(t, err2)
handleServerConnection(t, serverDone, nconn)
}()
return l, serverDone
}
// createTLSClientWithProtocol creates a configured RTSP client for RTSPS testing
func createTLSClientWithProtocol(addr string, protocol Protocol) *Client {
u, err := base.ParseURL("rtsps://" + addr + "/teststream")
if err != nil {
panic(err) // This should never happen in tests
}
c := &Client{
Scheme: u.Scheme,
Host: u.Host,
TLSConfig: &tls.Config{InsecureSkipVerify: true},
Protocol: &protocol,
}
return c
}
// testRTSPAnnounceWithProtocol is a helper function that tests RTSP announce with a specific protocol and media
func testRTSPAnnounceWithProtocol(t *testing.T, protocol Protocol, mediaFactory func() *description.Media) {
l, err := net.Listen("tcp", "localhost:0")
require.NoError(t, err)
defer l.Close()
serverDone := make(chan struct{})
defer func() { <-serverDone }()
go func() {
nconn, err2 := l.Accept()
require.NoError(t, err2)
handleServerConnection(t, serverDone, nconn)
}()
// Create media using the provided factory
media := mediaFactory()
desc := &description.Session{
Medias: []*description.Media{media},
}
u, err := base.ParseURL("rtsp://" + l.Addr().String() + "/teststream")
require.NoError(t, err)
c := Client{
Scheme: u.Scheme,
Host: u.Host,
}
// Set the protocol for this test
c.Protocol = &protocol
err = c.Start()
require.NoError(t, err)
defer c.Close()
// This should succeed - RTSP always sets secure=false regardless of media profile
_, err = c.Announce(u, desc)
require.NoError(t, err)
}
func TestClientAnnounceSecureProfileValidation(t *testing.T) {
// Test how secure flag is determined based on different protocol/scheme/profile combinations
//
// BEHAVIOR MATRIX:
// ┌─────────┬─────────┬──────────────┬────────────────────────────┐
// │Protocol │ Scheme │Media Profile │ secure flag result │
// ├─────────┼─────────┼──────────────┼────────────────────────────┤
// │ TCP │ RTSPS │ SAVP │ true (has secure profile) │
// │ TCP │ RTSPS │ RTP/AVP │ false (no secure profile) │
// │ UDP │ RTSPS │ Any │ true (scheme is rtsps) │
// │ TCP │ RTSP │ Any │ false (scheme is rtsp) │
// │ UDP │ RTSP │ Any │ false (scheme is rtsp) │
// └─────────┴─────────┴──────────────┴────────────────────────────┘
//
// Current implementation logic:
// if (Protocol == TCP && Scheme == "rtsps") {
// secure = hasSecureProfile // Check if any media has SAVP profile
// } else {
// secure = (Scheme == "rtsps") // Based on scheme only
// }
t.Run("TCP+RTSPS with secure profile - secure=true", func(t *testing.T) {
l, serverDone := setupTLSTestServer(t)
defer l.Close()
defer func() { <-serverDone }()
// Create a media with secure profile (SAVP)
media := createSecureMedia()
desc := &description.Session{Medias: []*description.Media{media}}
c := createTLSClientWithProtocol(l.Addr().String(), ProtocolTCP)
err := c.Start()
require.NoError(t, err)
defer c.Close()
// This should succeed - TCP+RTSPS with secure profile sets secure=true
u, err := base.ParseURL("rtsps://" + l.Addr().String() + "/teststream")
require.NoError(t, err)
_, err = c.Announce(u, desc)
require.NoError(t, err)
})
t.Run("TCP+RTSPS with non-secure profile - secure=false", func(t *testing.T) {
l, serverDone := setupTLSTestServer(t)
defer l.Close()
defer func() { <-serverDone }()
// Create a media with NON-secure profile (default RTP/AVP)
media := createNonSecureMedia()
desc := &description.Session{Medias: []*description.Media{media}}
c := createTLSClientWithProtocol(l.Addr().String(), ProtocolTCP)
err := c.Start()
require.NoError(t, err)
defer c.Close()
// This should succeed - TCP+RTSPS with non-secure profile sets secure=false
u, err := base.ParseURL("rtsps://" + l.Addr().String() + "/teststream")
require.NoError(t, err)
_, err = c.Announce(u, desc)
require.NoError(t, err)
})
t.Run("UDP+RTSPS with any profile - secure=true", func(t *testing.T) {
l, serverDone := setupTLSTestServer(t)
defer l.Close()
defer func() { <-serverDone }()
// Create a media with secure profile (SAVP)
media := createSecureMedia()
desc := &description.Session{Medias: []*description.Media{media}}
c := createTLSClientWithProtocol(l.Addr().String(), ProtocolUDP)
err := c.Start()
require.NoError(t, err)
defer c.Close()
// This should succeed - UDP+RTSPS always sets secure=true regardless of media profile
u, err := base.ParseURL("rtsps://" + l.Addr().String() + "/teststream")
require.NoError(t, err)
_, err = c.Announce(u, desc)
require.NoError(t, err)
})
t.Run("TCP+RTSP with any profile - secure=false", func(t *testing.T) {
// Create a media with regular profile (RTP/AVP - not secure)
testRTSPAnnounceWithProtocol(t, ProtocolTCP, createNonSecureMedia)
})
t.Run("UDP+RTSP with any profile - secure=false", func(t *testing.T) {
// Create a media with secure profile (just to show it doesn't matter for UDP+RTSP)
testRTSPAnnounceWithProtocol(t, ProtocolUDP, createSecureMedia)
})
t.Run("TCP+RTSPS with mixed profiles - secure=true if any profile is secure", func(t *testing.T) {
l, serverDone := setupTLSTestServer(t)
defer l.Close()
defer func() { <-serverDone }()
// Create multiple medias: one secure, one non-secure
mediaSecure := createSecureMedia()
mediaNonSecure := createAudioMedia()
desc := &description.Session{Medias: []*description.Media{mediaSecure, mediaNonSecure}}
c := createTLSClientWithProtocol(l.Addr().String(), ProtocolTCP)
err := c.Start()
require.NoError(t, err)
defer c.Close()
// This should succeed - TCP+RTSPS with mixed profiles, one secure sets secure=true
u, err := base.ParseURL("rtsps://" + l.Addr().String() + "/teststream")
require.NoError(t, err)
_, err = c.Announce(u, desc)
require.NoError(t, err)
})
}