golanggohlsrtmpwebrtcmedia-serverobs-studiortcprtmp-proxyrtmp-serverrtprtsprtsp-proxyrtsp-relayrtsp-serversrtstreamingwebrtc-proxy
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
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(), |
|
} |
|
}
|
|
|