diff --git a/internal/core/authentication.go b/internal/core/authentication.go index 671f570b..f77911a2 100644 --- a/internal/core/authentication.go +++ b/internal/core/authentication.go @@ -34,6 +34,15 @@ func checkCredential(right string, guess string) bool { return right == guess } +type errAuthentication struct { + message string +} + +// Error implements the error interface. +func (e *errAuthentication) Error() string { + return "authentication failed: " + e.message +} + type authProtocol string const ( @@ -43,16 +52,23 @@ const ( authProtocolWebRTC authProtocol = "webrtc" ) -func externalAuth( +type authCredentials struct { + query string + ip net.IP + user string + pass string + proto authProtocol + id *uuid.UUID + rtspRequest *base.Request + rtspBaseURL *url.URL + rtspNonce string +} + +func doExternalAuthentication( ur string, - ip string, - user string, - password string, path string, - protocol authProtocol, - id *uuid.UUID, publish bool, - query string, + credentials authCredentials, ) error { enc, _ := json.Marshal(struct { IP string `json:"ip"` @@ -64,19 +80,19 @@ func externalAuth( Action string `json:"action"` Query string `json:"query"` }{ - IP: ip, - User: user, - Password: password, + IP: credentials.ip.String(), + User: credentials.user, + Password: credentials.pass, Path: path, - Protocol: string(protocol), - ID: id, + Protocol: string(credentials.proto), + ID: credentials.id, Action: func() string { if publish { return "publish" } return "read" }(), - Query: query, + Query: credentials.query, }) res, err := http.Post(ur, "application/json", bytes.NewReader(enc)) if err != nil { @@ -86,28 +102,15 @@ func externalAuth( if res.StatusCode < 200 || res.StatusCode > 299 { if resBody, err := io.ReadAll(res.Body); err == nil && len(resBody) != 0 { - return fmt.Errorf("external authentication replied with code %d: %s", res.StatusCode, string(resBody)) + return fmt.Errorf("server replied with code %d: %s", res.StatusCode, string(resBody)) } - - return fmt.Errorf("external authentication replied with code %d", res.StatusCode) + return fmt.Errorf("server replied with code %d", res.StatusCode) } return nil } -type authCredentials struct { - query string - ip net.IP - user string - pass string - proto authProtocol - id *uuid.UUID - rtspRequest *base.Request - rtspBaseURL *url.URL - rtspNonce string -} - -func authenticate( +func doAuthentication( externalAuthenticationURL string, rtspAuthMethods conf.AuthMethods, pathName string, @@ -125,19 +128,14 @@ func authenticate( } if externalAuthenticationURL != "" { - err := externalAuth( + err := doExternalAuthentication( externalAuthenticationURL, - credentials.ip.String(), - credentials.user, - credentials.pass, pathName, - credentials.proto, - credentials.id, publish, - credentials.query, + credentials, ) if err != nil { - return fmt.Errorf("external authentication failed: %s", err) + return &errAuthentication{message: fmt.Sprintf("external authentication failed: %s", err)} } } @@ -157,7 +155,7 @@ func authenticate( if pathIPs != nil { if !ipEqualOrInRange(credentials.ip, pathIPs) { - return fmt.Errorf("IP '%s' not allowed", credentials.ip) + return &errAuthentication{message: fmt.Sprintf("IP %s not allowed", credentials.ip)} } } @@ -172,11 +170,11 @@ func authenticate( "IPCAM", credentials.rtspNonce) if err != nil { - return err + return &errAuthentication{message: err.Error()} } } else if !checkCredential(pathUser, credentials.user) || !checkCredential(pathPass, credentials.pass) { - return fmt.Errorf("invalid credentials") + return &errAuthentication{message: "invalid credentials"} } } diff --git a/internal/core/hls_http_server.go b/internal/core/hls_http_server.go index 8c56c19f..b6bc76c7 100644 --- a/internal/core/hls_http_server.go +++ b/internal/core/hls_http_server.go @@ -154,14 +154,18 @@ func (s *hlsHTTPServer) onRequest(ctx *gin.Context) { }, }) if res.err != nil { - if terr, ok := res.err.(pathErrAuth); ok { + if terr, ok := res.err.(*errAuthentication); ok { if !hasCredentials { ctx.Header("WWW-Authenticate", `Basic realm="mediamtx"`) ctx.Writer.WriteHeader(http.StatusUnauthorized) return } - s.Log(logger.Info, "authentication failed: %v", terr.wrapped) + ip := ctx.ClientIP() + _, port, _ := net.SplitHostPort(ctx.Request.RemoteAddr) + remoteAddr := net.JoinHostPort(ip, port) + + s.Log(logger.Info, "connection %v failed to authenticate: %v", remoteAddr, terr.message) ctx.Writer.WriteHeader(http.StatusUnauthorized) return } diff --git a/internal/core/path.go b/internal/core/path.go index c6c1bf4b..598bd534 100644 --- a/internal/core/path.go +++ b/internal/core/path.go @@ -24,21 +24,12 @@ func newEmptyTimer() *time.Timer { return t } -type pathErrAuth struct { - wrapped error -} - -// Error implements the error interface. -func (e pathErrAuth) Error() string { - return "authentication failed" -} - -type pathErrNoOnePublishing struct { +type errPathNoOnePublishing struct { pathName string } // Error implements the error interface. -func (e pathErrNoOnePublishing) Error() string { +func (e errPathNoOnePublishing) Error() string { return fmt.Sprintf("no one is publishing to path '%s'", e.pathName) } @@ -739,7 +730,7 @@ func (pa *path) handleDescribe(req pathDescribeReq) { return } - req.res <- pathDescribeRes{err: pathErrNoOnePublishing{pathName: pa.name}} + req.res <- pathDescribeRes{err: errPathNoOnePublishing{pathName: pa.name}} } func (pa *path) handlePublisherRemove(req pathPublisherRemoveReq) { @@ -855,7 +846,7 @@ func (pa *path) handleReaderAdd(req pathReaderAddReq) { return } - req.res <- pathReaderSetupPlayRes{err: pathErrNoOnePublishing{pathName: pa.name}} + req.res <- pathReaderSetupPlayRes{err: errPathNoOnePublishing{pathName: pa.name}} } func (pa *path) handleReaderAddPost(req pathReaderAddReq) { diff --git a/internal/core/path_manager.go b/internal/core/path_manager.go index c61dc707..e048006f 100644 --- a/internal/core/path_manager.go +++ b/internal/core/path_manager.go @@ -235,10 +235,10 @@ outer: continue } - err = authenticate(pm.externalAuthenticationURL, pm.authMethods, + err = doAuthentication(pm.externalAuthenticationURL, pm.authMethods, req.name, pathConf, req.publish, req.credentials) if err != nil { - req.res <- pathGetConfForPathRes{err: pathErrAuth{wrapped: err}} + req.res <- pathGetConfForPathRes{err: err} continue } @@ -251,9 +251,9 @@ outer: continue } - err = authenticate(pm.externalAuthenticationURL, pm.authMethods, req.pathName, pathConf, false, req.credentials) + err = doAuthentication(pm.externalAuthenticationURL, pm.authMethods, req.pathName, pathConf, false, req.credentials) if err != nil { - req.res <- pathDescribeRes{err: pathErrAuth{wrapped: err}} + req.res <- pathDescribeRes{err: err} continue } @@ -272,9 +272,9 @@ outer: } if !req.skipAuth { - err = authenticate(pm.externalAuthenticationURL, pm.authMethods, req.pathName, pathConf, false, req.credentials) + err = doAuthentication(pm.externalAuthenticationURL, pm.authMethods, req.pathName, pathConf, false, req.credentials) if err != nil { - req.res <- pathReaderSetupPlayRes{err: pathErrAuth{wrapped: err}} + req.res <- pathReaderSetupPlayRes{err: err} continue } } @@ -294,9 +294,9 @@ outer: } if !req.skipAuth { - err = authenticate(pm.externalAuthenticationURL, pm.authMethods, req.pathName, pathConf, true, req.credentials) + err = doAuthentication(pm.externalAuthenticationURL, pm.authMethods, req.pathName, pathConf, true, req.credentials) if err != nil { - req.res <- pathPublisherAnnounceRes{err: pathErrAuth{wrapped: err}} + req.res <- pathPublisherAnnounceRes{err: err} continue } } diff --git a/internal/core/rtmp_conn.go b/internal/core/rtmp_conn.go index 906a90b4..75d33df1 100644 --- a/internal/core/rtmp_conn.go +++ b/internal/core/rtmp_conn.go @@ -365,10 +365,10 @@ func (c *rtmpConn) runRead(ctx context.Context, u *url.URL) error { }) if res.err != nil { - if terr, ok := res.err.(pathErrAuth); ok { + if terr, ok := res.err.(*errAuthentication); ok { // wait some seconds to stop brute force attacks <-time.After(rtmpConnPauseAfterAuthError) - return terr.wrapped + return terr } return res.err } @@ -780,10 +780,10 @@ func (c *rtmpConn) runPublish(u *url.URL) error { }) if res.err != nil { - if terr, ok := res.err.(pathErrAuth); ok { + if terr, ok := res.err.(*errAuthentication); ok { // wait some seconds to stop brute force attacks <-time.After(rtmpConnPauseAfterAuthError) - return terr.wrapped + return terr } return res.err } diff --git a/internal/core/rtsp_conn.go b/internal/core/rtsp_conn.go index da59c41c..48178291 100644 --- a/internal/core/rtsp_conn.go +++ b/internal/core/rtsp_conn.go @@ -155,11 +155,11 @@ func (c *rtspConn) onDescribe(ctx *gortsplib.ServerHandlerOnDescribeCtx, if res.err != nil { switch terr := res.err.(type) { - case pathErrAuth: - res, err := c.handleAuthError(terr.wrapped) + case *errAuthentication: + res, err := c.handleAuthError(terr) return res, nil, err - case pathErrNoOnePublishing: + case errPathNoOnePublishing: return &base.Response{ StatusCode: base.StatusNotFound, }, nil, res.err diff --git a/internal/core/rtsp_session.go b/internal/core/rtsp_session.go index d9438106..b3354ed5 100644 --- a/internal/core/rtsp_session.go +++ b/internal/core/rtsp_session.go @@ -140,8 +140,8 @@ func (s *rtspSession) onAnnounce(c *rtspConn, ctx *gortsplib.ServerHandlerOnAnno if res.err != nil { switch terr := res.err.(type) { - case pathErrAuth: - return c.handleAuthError(terr.wrapped) + case *errAuthentication: + return c.handleAuthError(terr) default: return &base.Response{ @@ -219,11 +219,11 @@ func (s *rtspSession) onSetup(c *rtspConn, ctx *gortsplib.ServerHandlerOnSetupCt if res.err != nil { switch terr := res.err.(type) { - case pathErrAuth: - res, err := c.handleAuthError(terr.wrapped) + case *errAuthentication: + res, err := c.handleAuthError(terr) return res, nil, err - case pathErrNoOnePublishing: + case errPathNoOnePublishing: return &base.Response{ StatusCode: base.StatusNotFound, }, nil, res.err diff --git a/internal/core/webrtc_http_server.go b/internal/core/webrtc_http_server.go index 1c48ed5b..d5688fa2 100644 --- a/internal/core/webrtc_http_server.go +++ b/internal/core/webrtc_http_server.go @@ -293,6 +293,7 @@ func (s *webRTCHTTPServer) onRequest(ctx *gin.Context) { ip := ctx.ClientIP() _, port, _ := net.SplitHostPort(ctx.Request.RemoteAddr) + remoteAddr := net.JoinHostPort(ip, port) user, pass, hasCredentials := ctx.Request.BasicAuth() // if request doesn't belong to a session, check authentication here @@ -309,14 +310,14 @@ func (s *webRTCHTTPServer) onRequest(ctx *gin.Context) { }, }) if res.err != nil { - if terr, ok := res.err.(pathErrAuth); ok { + if terr, ok := res.err.(*errAuthentication); ok { if !hasCredentials { ctx.Header("WWW-Authenticate", `Basic realm="mediamtx"`) ctx.Writer.WriteHeader(http.StatusUnauthorized) return } - s.Log(logger.Info, "authentication failed: %v", terr.wrapped) + s.Log(logger.Info, "connection %v failed to authenticate: %v", remoteAddr, terr.message) ctx.Writer.WriteHeader(http.StatusUnauthorized) return } @@ -358,7 +359,7 @@ func (s *webRTCHTTPServer) onRequest(ctx *gin.Context) { res := s.parent.sessionNew(webRTCSessionNewReq{ pathName: dir, - remoteAddr: net.JoinHostPort(ip, port), + remoteAddr: remoteAddr, query: ctx.Request.URL.RawQuery, user: user, pass: pass, diff --git a/internal/core/webrtc_session.go b/internal/core/webrtc_session.go index fcfa170e..8bc9d03c 100644 --- a/internal/core/webrtc_session.go +++ b/internal/core/webrtc_session.go @@ -239,7 +239,7 @@ func (s *webRTCSession) runPublish() (int, error) { }, }) if res.err != nil { - if _, ok := res.err.(pathErrAuth); ok { + if _, ok := res.err.(*errAuthentication); ok { return http.StatusUnauthorized, res.err } return http.StatusBadRequest, res.err @@ -362,7 +362,7 @@ func (s *webRTCSession) runRead() (int, error) { }, }) if res.err != nil { - if _, ok := res.err.(pathErrAuth); ok { + if _, ok := res.err.(*errAuthentication); ok { return http.StatusUnauthorized, res.err } if strings.HasPrefix(res.err.Error(), "no one is publishing") {