Ready-to-use SRT / WebRTC / RTSP / RTMP / LL-HLS media server and media proxy that allows to read, publish, proxy, record and playback video and audio streams.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

215 lines
5.1 KiB

package rtsp
import (
"errors"
"fmt"
"net"
"time"
"github.com/bluenviron/gortsplib/v4"
"github.com/bluenviron/gortsplib/v4/pkg/auth"
"github.com/bluenviron/gortsplib/v4/pkg/base"
"github.com/bluenviron/gortsplib/v4/pkg/headers"
"github.com/google/uuid"
"github.com/bluenviron/mediamtx/internal/conf"
"github.com/bluenviron/mediamtx/internal/defs"
"github.com/bluenviron/mediamtx/internal/externalcmd"
"github.com/bluenviron/mediamtx/internal/hooks"
"github.com/bluenviron/mediamtx/internal/logger"
)
const (
rtspPauseAfterAuthError = 2 * time.Second
)
type conn struct {
isTLS bool
rtspAddress string
authMethods []headers.AuthMethod
readTimeout conf.StringDuration
runOnConnect string
runOnConnectRestart bool
runOnDisconnect string
externalCmdPool *externalcmd.Pool
pathManager defs.PathManager
rconn *gortsplib.ServerConn
rserver *gortsplib.Server
parent *Server
uuid uuid.UUID
created time.Time
onDisconnectHook func()
authNonce string
authFailures int
}
func (c *conn) initialize() {
c.uuid = uuid.New()
c.created = time.Now()
c.Log(logger.Info, "opened")
desc := defs.APIPathSourceOrReader{
Type: func() string {
if c.isTLS {
return "rtspsConn"
}
return "conn"
}(),
ID: c.uuid.String(),
}
c.onDisconnectHook = hooks.OnConnect(hooks.OnConnectParams{
Logger: c,
ExternalCmdPool: c.externalCmdPool,
RunOnConnect: c.runOnConnect,
RunOnConnectRestart: c.runOnConnectRestart,
RunOnDisconnect: c.runOnDisconnect,
RTSPAddress: c.rtspAddress,
Desc: desc,
})
}
// Log implements logger.Writer.
func (c *conn) Log(level logger.Level, format string, args ...interface{}) {
c.parent.Log(level, "[conn %v] "+format, append([]interface{}{c.rconn.NetConn().RemoteAddr()}, args...)...)
}
// Conn returns the RTSP connection.
func (c *conn) Conn() *gortsplib.ServerConn {
return c.rconn
}
func (c *conn) remoteAddr() net.Addr {
return c.rconn.NetConn().RemoteAddr()
}
func (c *conn) ip() net.IP {
return c.rconn.NetConn().RemoteAddr().(*net.TCPAddr).IP
}
// onClose is called by rtspServer.
func (c *conn) onClose(err error) {
c.Log(logger.Info, "closed: %v", err)
c.onDisconnectHook()
}
// onRequest is called by rtspServer.
func (c *conn) onRequest(req *base.Request) {
c.Log(logger.Debug, "[c->s] %v", req)
}
// OnResponse is called by rtspServer.
func (c *conn) OnResponse(res *base.Response) {
c.Log(logger.Debug, "[s->c] %v", res)
}
// onDescribe is called by rtspServer.
func (c *conn) onDescribe(ctx *gortsplib.ServerHandlerOnDescribeCtx,
) (*base.Response, *gortsplib.ServerStream, error) {
if len(ctx.Path) == 0 || ctx.Path[0] != '/' {
return &base.Response{
StatusCode: base.StatusBadRequest,
}, nil, fmt.Errorf("invalid path")
}
ctx.Path = ctx.Path[1:]
if c.authNonce == "" {
var err error
c.authNonce, err = auth.GenerateNonce()
if err != nil {
return &base.Response{
StatusCode: base.StatusInternalServerError,
}, nil, err
}
}
res := c.pathManager.Describe(defs.PathDescribeReq{
AccessRequest: defs.PathAccessRequest{
Name: ctx.Path,
Query: ctx.Query,
IP: c.ip(),
Proto: defs.AuthProtocolRTSP,
ID: &c.uuid,
RTSPRequest: ctx.Request,
RTSPNonce: c.authNonce,
},
})
if res.Err != nil {
var terr defs.AuthenticationError
if errors.As(res.Err, &terr) {
res, err := c.handleAuthError(terr)
return res, nil, err
}
var terr2 defs.PathNoOnePublishingError
if errors.As(res.Err, &terr2) {
return &base.Response{
StatusCode: base.StatusNotFound,
}, nil, res.Err
}
return &base.Response{
StatusCode: base.StatusBadRequest,
}, nil, res.Err
}
if res.Redirect != "" {
return &base.Response{
StatusCode: base.StatusMovedPermanently,
Header: base.Header{
"Location": base.HeaderValue{res.Redirect},
},
}, nil, nil
}
var stream *gortsplib.ServerStream
if !c.isTLS {
stream = res.Stream.RTSPStream(c.rserver)
} else {
stream = res.Stream.RTSPSStream(c.rserver)
}
return &base.Response{
StatusCode: base.StatusOK,
}, stream, nil
}
func (c *conn) handleAuthError(authErr error) (*base.Response, error) {
c.authFailures++
// VLC with login prompt sends 4 requests:
// 1) without credentials
// 2) with password but without username
// 3) without credentials
// 4) with password and username
// therefore we must allow up to 3 failures
if c.authFailures <= 3 {
return &base.Response{
StatusCode: base.StatusUnauthorized,
Header: base.Header{
"WWW-Authenticate": auth.GenerateWWWAuthenticate(c.authMethods, "IPCAM", c.authNonce),
},
}, nil
}
// wait some seconds to stop brute force attacks
<-time.After(rtspPauseAfterAuthError)
return &base.Response{
StatusCode: base.StatusUnauthorized,
}, authErr
}
func (c *conn) apiItem() *defs.APIRTSPConn {
return &defs.APIRTSPConn{
ID: c.uuid,
Created: c.created,
RemoteAddr: c.remoteAddr().String(),
BytesReceived: c.rconn.BytesReceived(),
BytesSent: c.rconn.BytesSent(),
}
}