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.
235 lines
5.2 KiB
235 lines
5.2 KiB
package core |
|
|
|
import ( |
|
"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/logger" |
|
) |
|
|
|
const ( |
|
rtspPauseAfterAuthError = 2 * time.Second |
|
) |
|
|
|
type rtspConnParent interface { |
|
logger.Writer |
|
getISTLS() bool |
|
getServer() *gortsplib.Server |
|
} |
|
|
|
type rtspConn struct { |
|
*conn |
|
|
|
isTLS bool |
|
rtspAddress string |
|
authMethods []headers.AuthMethod |
|
readTimeout conf.StringDuration |
|
pathManager *pathManager |
|
rconn *gortsplib.ServerConn |
|
parent rtspConnParent |
|
|
|
uuid uuid.UUID |
|
created time.Time |
|
authNonce string |
|
authFailures int |
|
} |
|
|
|
func newRTSPConn( |
|
isTLS bool, |
|
rtspAddress string, |
|
authMethods []headers.AuthMethod, |
|
readTimeout conf.StringDuration, |
|
runOnConnect string, |
|
runOnConnectRestart bool, |
|
runOnDisconnect string, |
|
externalCmdPool *externalcmd.Pool, |
|
pathManager *pathManager, |
|
conn *gortsplib.ServerConn, |
|
parent rtspConnParent, |
|
) *rtspConn { |
|
c := &rtspConn{ |
|
isTLS: isTLS, |
|
rtspAddress: rtspAddress, |
|
authMethods: authMethods, |
|
readTimeout: readTimeout, |
|
pathManager: pathManager, |
|
rconn: conn, |
|
parent: parent, |
|
uuid: uuid.New(), |
|
created: time.Now(), |
|
} |
|
|
|
c.conn = newConn( |
|
rtspAddress, |
|
runOnConnect, |
|
runOnConnectRestart, |
|
runOnDisconnect, |
|
externalCmdPool, |
|
c, |
|
) |
|
|
|
c.Log(logger.Info, "opened") |
|
|
|
c.conn.open(defs.APIPathSourceOrReader{ |
|
Type: func() string { |
|
if isTLS { |
|
return "rtspsConn" |
|
} |
|
return "rtspConn" |
|
}(), |
|
ID: c.uuid.String(), |
|
}) |
|
|
|
return c |
|
} |
|
|
|
func (c *rtspConn) 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 *rtspConn) Conn() *gortsplib.ServerConn { |
|
return c.rconn |
|
} |
|
|
|
func (c *rtspConn) remoteAddr() net.Addr { |
|
return c.rconn.NetConn().RemoteAddr() |
|
} |
|
|
|
func (c *rtspConn) ip() net.IP { |
|
return c.rconn.NetConn().RemoteAddr().(*net.TCPAddr).IP |
|
} |
|
|
|
// onClose is called by rtspServer. |
|
func (c *rtspConn) onClose(err error) { |
|
c.Log(logger.Info, "closed: %v", err) |
|
|
|
c.conn.close() |
|
} |
|
|
|
// onRequest is called by rtspServer. |
|
func (c *rtspConn) onRequest(req *base.Request) { |
|
c.Log(logger.Debug, "[c->s] %v", req) |
|
} |
|
|
|
// OnResponse is called by rtspServer. |
|
func (c *rtspConn) OnResponse(res *base.Response) { |
|
c.Log(logger.Debug, "[s->c] %v", res) |
|
} |
|
|
|
// onDescribe is called by rtspServer. |
|
func (c *rtspConn) 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(pathDescribeReq{ |
|
accessRequest: pathAccessRequest{ |
|
name: ctx.Path, |
|
query: ctx.Query, |
|
ip: c.ip(), |
|
proto: authProtocolRTSP, |
|
id: &c.uuid, |
|
rtspRequest: ctx.Request, |
|
rtspNonce: c.authNonce, |
|
}, |
|
}) |
|
|
|
if res.err != nil { |
|
switch terr := res.err.(type) { |
|
case *errAuthentication: |
|
res, err := c.handleAuthError(terr) |
|
return res, nil, err |
|
|
|
case errPathNoOnePublishing: |
|
return &base.Response{ |
|
StatusCode: base.StatusNotFound, |
|
}, nil, res.err |
|
|
|
default: |
|
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.parent.getISTLS() { |
|
stream = res.stream.RTSPStream(c.parent.getServer()) |
|
} else { |
|
stream = res.stream.RTSPSStream(c.parent.getServer()) |
|
} |
|
|
|
return &base.Response{ |
|
StatusCode: base.StatusOK, |
|
}, stream, nil |
|
} |
|
|
|
func (c *rtspConn) 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 *rtspConn) apiItem() *defs.APIRTSPConn { |
|
return &defs.APIRTSPConn{ |
|
ID: c.uuid, |
|
Created: c.created, |
|
RemoteAddr: c.remoteAddr().String(), |
|
BytesReceived: c.rconn.BytesReceived(), |
|
BytesSent: c.rconn.BytesSent(), |
|
} |
|
}
|
|
|