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.
211 lines
5.0 KiB
211 lines
5.0 KiB
package core |
|
|
|
import ( |
|
"fmt" |
|
"net" |
|
"time" |
|
|
|
"github.com/bluenviron/gortsplib/v3" |
|
"github.com/bluenviron/gortsplib/v3/pkg/auth" |
|
"github.com/bluenviron/gortsplib/v3/pkg/base" |
|
"github.com/bluenviron/gortsplib/v3/pkg/headers" |
|
"github.com/google/uuid" |
|
|
|
"github.com/bluenviron/mediamtx/internal/conf" |
|
"github.com/bluenviron/mediamtx/internal/externalcmd" |
|
"github.com/bluenviron/mediamtx/internal/logger" |
|
) |
|
|
|
const ( |
|
rtspConnPauseAfterAuthError = 2 * time.Second |
|
) |
|
|
|
type rtspConnParent interface { |
|
logger.Writer |
|
} |
|
|
|
type rtspConn struct { |
|
rtspAddress string |
|
authMethods []headers.AuthMethod |
|
readTimeout conf.StringDuration |
|
runOnConnect string |
|
runOnConnectRestart bool |
|
externalCmdPool *externalcmd.Pool |
|
pathManager *pathManager |
|
conn *gortsplib.ServerConn |
|
parent rtspConnParent |
|
|
|
uuid uuid.UUID |
|
created time.Time |
|
onConnectCmd *externalcmd.Cmd |
|
authNonce string |
|
authFailures int |
|
} |
|
|
|
func newRTSPConn( |
|
rtspAddress string, |
|
authMethods []headers.AuthMethod, |
|
readTimeout conf.StringDuration, |
|
runOnConnect string, |
|
runOnConnectRestart bool, |
|
externalCmdPool *externalcmd.Pool, |
|
pathManager *pathManager, |
|
conn *gortsplib.ServerConn, |
|
parent rtspConnParent, |
|
) *rtspConn { |
|
c := &rtspConn{ |
|
rtspAddress: rtspAddress, |
|
authMethods: authMethods, |
|
readTimeout: readTimeout, |
|
runOnConnect: runOnConnect, |
|
runOnConnectRestart: runOnConnectRestart, |
|
externalCmdPool: externalCmdPool, |
|
pathManager: pathManager, |
|
conn: conn, |
|
parent: parent, |
|
uuid: uuid.New(), |
|
created: time.Now(), |
|
} |
|
|
|
c.Log(logger.Info, "opened") |
|
|
|
if c.runOnConnect != "" { |
|
c.Log(logger.Info, "runOnConnect command started") |
|
_, port, _ := net.SplitHostPort(c.rtspAddress) |
|
c.onConnectCmd = externalcmd.NewCmd( |
|
c.externalCmdPool, |
|
c.runOnConnect, |
|
c.runOnConnectRestart, |
|
externalcmd.Environment{ |
|
"RTSP_PATH": "", |
|
"RTSP_PORT": port, |
|
}, |
|
func(co int) { |
|
c.Log(logger.Info, "runOnInit command exited with code %d", co) |
|
}) |
|
} |
|
|
|
return c |
|
} |
|
|
|
func (c *rtspConn) Log(level logger.Level, format string, args ...interface{}) { |
|
c.parent.Log(level, "[conn %v] "+format, append([]interface{}{c.conn.NetConn().RemoteAddr()}, args...)...) |
|
} |
|
|
|
// Conn returns the RTSP connection. |
|
func (c *rtspConn) Conn() *gortsplib.ServerConn { |
|
return c.conn |
|
} |
|
|
|
func (c *rtspConn) remoteAddr() net.Addr { |
|
return c.conn.NetConn().RemoteAddr() |
|
} |
|
|
|
func (c *rtspConn) ip() net.IP { |
|
return c.conn.NetConn().RemoteAddr().(*net.TCPAddr).IP |
|
} |
|
|
|
// onClose is called by rtspServer. |
|
func (c *rtspConn) onClose(err error) { |
|
c.Log(logger.Info, "closed (%v)", err) |
|
|
|
if c.onConnectCmd != nil { |
|
c.onConnectCmd.Close() |
|
c.Log(logger.Info, "runOnConnect command stopped") |
|
} |
|
} |
|
|
|
// 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 == "" { |
|
c.authNonce = auth.GenerateNonce() |
|
} |
|
|
|
res := c.pathManager.describe(pathDescribeReq{ |
|
pathName: ctx.Path, |
|
url: ctx.Request.URL, |
|
credentials: authCredentials{ |
|
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 pathErrAuth: |
|
res, err := c.handleAuthError(terr.wrapped) |
|
return res, nil, err |
|
|
|
case pathErrNoOnePublishing: |
|
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 |
|
} |
|
|
|
return &base.Response{ |
|
StatusCode: base.StatusOK, |
|
}, res.stream.rtspStream, 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(rtspConnPauseAfterAuthError) |
|
|
|
return &base.Response{ |
|
StatusCode: base.StatusUnauthorized, |
|
}, authErr |
|
}
|
|
|