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.
294 lines
7.2 KiB
294 lines
7.2 KiB
package core |
|
|
|
import ( |
|
"errors" |
|
"fmt" |
|
"net" |
|
"time" |
|
|
|
"github.com/aler9/gortsplib" |
|
"github.com/aler9/gortsplib/pkg/auth" |
|
"github.com/aler9/gortsplib/pkg/base" |
|
"github.com/aler9/gortsplib/pkg/headers" |
|
|
|
"github.com/aler9/rtsp-simple-server/internal/conf" |
|
"github.com/aler9/rtsp-simple-server/internal/externalcmd" |
|
"github.com/aler9/rtsp-simple-server/internal/logger" |
|
) |
|
|
|
const ( |
|
rtspConnPauseAfterAuthError = 2 * time.Second |
|
) |
|
|
|
type rtspConnParent interface { |
|
log(logger.Level, string, ...interface{}) |
|
} |
|
|
|
type rtspConn struct { |
|
externalAuthenticationURL string |
|
rtspAddress string |
|
authMethods []headers.AuthMethod |
|
readTimeout conf.StringDuration |
|
runOnConnect string |
|
runOnConnectRestart bool |
|
externalCmdPool *externalcmd.Pool |
|
pathManager *pathManager |
|
conn *gortsplib.ServerConn |
|
parent rtspConnParent |
|
|
|
onConnectCmd *externalcmd.Cmd |
|
authUser string |
|
authPass string |
|
authValidator *auth.Validator |
|
authFailures int |
|
} |
|
|
|
func newRTSPConn( |
|
externalAuthenticationURL string, |
|
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{ |
|
externalAuthenticationURL: externalAuthenticationURL, |
|
rtspAddress: rtspAddress, |
|
authMethods: authMethods, |
|
readTimeout: readTimeout, |
|
runOnConnect: runOnConnect, |
|
runOnConnectRestart: runOnConnectRestart, |
|
externalCmdPool: externalCmdPool, |
|
pathManager: pathManager, |
|
conn: conn, |
|
parent: parent, |
|
} |
|
|
|
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) ip() net.IP { |
|
return c.conn.NetConn().RemoteAddr().(*net.TCPAddr).IP |
|
} |
|
|
|
func (c *rtspConn) authenticate( |
|
pathName string, |
|
pathIPs []interface{}, |
|
pathUser conf.Credential, |
|
pathPass conf.Credential, |
|
action string, |
|
req *base.Request, |
|
query string, |
|
) error { |
|
if c.externalAuthenticationURL != "" { |
|
username := "" |
|
password := "" |
|
|
|
var auth headers.Authorization |
|
err := auth.Read(req.Header["Authorization"]) |
|
if err == nil && auth.Method == headers.AuthBasic { |
|
username = auth.BasicUser |
|
password = auth.BasicPass |
|
} |
|
|
|
err = externalAuth( |
|
c.externalAuthenticationURL, |
|
c.ip().String(), |
|
username, |
|
password, |
|
pathName, |
|
action, |
|
query) |
|
if err != nil { |
|
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 pathErrAuthCritical{ |
|
message: "unauthorized: " + err.Error(), |
|
response: &base.Response{ |
|
StatusCode: base.StatusUnauthorized, |
|
}, |
|
} |
|
} |
|
|
|
v := "IPCAM" |
|
return pathErrAuthNotCritical{ |
|
message: "unauthorized: " + err.Error(), |
|
response: &base.Response{ |
|
StatusCode: base.StatusUnauthorized, |
|
Header: base.Header{ |
|
"WWW-Authenticate": headers.Authenticate{ |
|
Method: headers.AuthBasic, |
|
Realm: &v, |
|
}.Write(), |
|
}, |
|
}, |
|
} |
|
} |
|
} |
|
|
|
if pathIPs != nil { |
|
ip := c.ip() |
|
if !ipEqualOrInRange(ip, pathIPs) { |
|
return pathErrAuthCritical{ |
|
message: fmt.Sprintf("IP '%s' not allowed", ip), |
|
response: &base.Response{ |
|
StatusCode: base.StatusUnauthorized, |
|
}, |
|
} |
|
} |
|
} |
|
|
|
if pathUser != "" { |
|
// reset authValidator every time the credentials change |
|
if c.authValidator == nil || c.authUser != string(pathUser) || c.authPass != string(pathPass) { |
|
c.authUser = string(pathUser) |
|
c.authPass = string(pathPass) |
|
c.authValidator = auth.NewValidator(string(pathUser), string(pathPass), c.authMethods) |
|
} |
|
|
|
err := c.authValidator.ValidateRequest(req) |
|
if err != nil { |
|
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 pathErrAuthCritical{ |
|
message: "unauthorized: " + err.Error(), |
|
response: &base.Response{ |
|
StatusCode: base.StatusUnauthorized, |
|
}, |
|
} |
|
} |
|
|
|
return pathErrAuthNotCritical{ |
|
response: &base.Response{ |
|
StatusCode: base.StatusUnauthorized, |
|
Header: base.Header{ |
|
"WWW-Authenticate": c.authValidator.Header(), |
|
}, |
|
}, |
|
} |
|
} |
|
|
|
// login successful, reset authFailures |
|
c.authFailures = 0 |
|
} |
|
|
|
return nil |
|
} |
|
|
|
// 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) { |
|
res := c.pathManager.onDescribe(pathDescribeReq{ |
|
pathName: ctx.Path, |
|
url: ctx.Request.URL, |
|
authenticate: func( |
|
pathIPs []interface{}, |
|
pathUser conf.Credential, |
|
pathPass conf.Credential, |
|
) error { |
|
return c.authenticate(ctx.Path, pathIPs, pathUser, pathPass, "read", ctx.Request, ctx.Query) |
|
}, |
|
}) |
|
|
|
if res.err != nil { |
|
switch terr := res.err.(type) { |
|
case pathErrAuthNotCritical: |
|
c.log(logger.Debug, "non-critical authentication error: %s", terr.message) |
|
return terr.response, nil, nil |
|
|
|
case pathErrAuthCritical: |
|
// wait some seconds to stop brute force attacks |
|
<-time.After(rtspConnPauseAfterAuthError) |
|
|
|
return terr.response, nil, errors.New(terr.message) |
|
|
|
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 |
|
}
|
|
|