Files
cameradar/internal/attack/rtsp_test.go
T
Brendan Le Glaunec e81eeb0c4d feat: v6 rewrite
2026-01-27 22:11:17 +01:00

167 lines
4.1 KiB
Go

package attack_test
import (
"errors"
"net"
"net/netip"
"strings"
"testing"
"github.com/bluenviron/gortsplib/v5"
"github.com/bluenviron/gortsplib/v5/pkg/auth"
"github.com/bluenviron/gortsplib/v5/pkg/base"
"github.com/bluenviron/gortsplib/v5/pkg/description"
"github.com/bluenviron/gortsplib/v5/pkg/format"
"github.com/bluenviron/gortsplib/v5/pkg/headers"
"github.com/bluenviron/gortsplib/v5/pkg/liberrors"
"github.com/stretchr/testify/require"
)
type rtspServerConfig struct {
allowAll bool
allowedRoute string
requireAuth bool
username string
password string
authMethod headers.AuthMethod
authHeader base.HeaderValue
failOnAuth bool
setupStatus base.StatusCode
}
type testServerHandler struct {
stream *gortsplib.ServerStream
allowAll bool
allowedRoute string
requireAuth bool
username string
password string
authHeader base.HeaderValue
failOnAuth bool
setupStatus base.StatusCode
}
func (h *testServerHandler) OnDescribe(ctx *gortsplib.ServerHandlerOnDescribeCtx) (*base.Response, *gortsplib.ServerStream, error) {
if !h.routeAllowed(ctx.Path) {
return &base.Response{StatusCode: base.StatusNotFound}, nil, nil
}
if h.failOnAuth && len(ctx.Request.Header["Authorization"]) > 0 {
return &base.Response{StatusCode: base.StatusBadRequest}, nil, errors.New("forced auth failure")
}
if h.requireAuth && !ctx.Conn.VerifyCredentials(ctx.Request, h.username, h.password) {
return &base.Response{
StatusCode: base.StatusUnauthorized,
Header: base.Header{
"WWW-Authenticate": h.authHeader,
},
}, nil, liberrors.ErrServerAuth{}
}
return &base.Response{StatusCode: base.StatusOK}, h.stream, nil
}
func (h *testServerHandler) OnSetup(ctx *gortsplib.ServerHandlerOnSetupCtx) (*base.Response, *gortsplib.ServerStream, error) {
if !h.routeAllowed(ctx.Path) {
return &base.Response{StatusCode: base.StatusNotFound}, nil, nil
}
if h.requireAuth && !ctx.Conn.VerifyCredentials(ctx.Request, h.username, h.password) {
return &base.Response{
StatusCode: base.StatusUnauthorized,
Header: base.Header{
"WWW-Authenticate": h.authHeader,
},
}, nil, liberrors.ErrServerAuth{}
}
status := base.StatusOK
if h.setupStatus != 0 {
status = h.setupStatus
}
return &base.Response{StatusCode: status}, h.stream, nil
}
func (h *testServerHandler) routeAllowed(path string) bool {
path = strings.TrimLeft(path, "/")
return h.allowAll || path == h.allowedRoute
}
func startRTSPServer(t *testing.T, cfg rtspServerConfig) (netip.Addr, uint16) {
t.Helper()
handler := &testServerHandler{
allowAll: cfg.allowAll,
allowedRoute: cfg.allowedRoute,
requireAuth: cfg.requireAuth,
username: cfg.username,
password: cfg.password,
failOnAuth: cfg.failOnAuth,
setupStatus: cfg.setupStatus,
}
if len(cfg.authHeader) > 0 {
handler.authHeader = cfg.authHeader
} else {
authHeader := headers.Authenticate{
Method: cfg.authMethod,
Realm: "cameradar",
}
if cfg.authMethod == headers.AuthMethodDigest {
authHeader.Nonce = "nonce"
}
handler.authHeader = authHeader.Marshal()
}
server := &gortsplib.Server{
Handler: handler,
RTSPAddress: "127.0.0.1:0",
AuthMethods: authMethods(cfg.authMethod),
}
err := server.Start()
require.NoError(t, err)
t.Cleanup(server.Close)
desc := &description.Session{
Medias: []*description.Media{{
Type: description.MediaTypeVideo,
Formats: []format.Format{&format.H264{
PayloadTyp: 96,
PacketizationMode: 1,
}},
}},
}
stream := &gortsplib.ServerStream{
Server: server,
Desc: desc,
}
err = stream.Initialize()
require.NoError(t, err)
t.Cleanup(stream.Close)
handler.stream = stream
listener := server.NetListener()
require.NotNil(t, listener)
tcpAddr, ok := listener.Addr().(*net.TCPAddr)
require.True(t, ok)
return netip.MustParseAddr("127.0.0.1"), uint16(tcpAddr.Port)
}
func authMethods(method headers.AuthMethod) []auth.VerifyMethod {
switch method {
case headers.AuthMethodDigest:
return []auth.VerifyMethod{auth.VerifyMethodDigestMD5}
case headers.AuthMethodBasic:
return []auth.VerifyMethod{auth.VerifyMethodBasic}
default:
return nil
}
}