hls: return JSON with error message in case path conf is not available (#5655)

this behavior is aligned with WebRTC one.
This commit is contained in:
Alessandro Ros
2026-04-08 23:26:04 +02:00
committed by GitHub
parent c9ec74c19b
commit c7826d406b
8 changed files with 170 additions and 76 deletions
+11 -9
View File
@@ -2,6 +2,7 @@
package api //nolint:revive
import (
"fmt"
"net"
"net/http"
"reflect"
@@ -206,7 +207,14 @@ func (a *API) writeError(ctx *gin.Context, status int, err error) {
a.Log(logger.Error, err.Error())
// add error to response
ctx.JSON(status, &defs.APIError{
ctx.AbortWithStatusJSON(status, &defs.APIError{
Status: defs.APIErrorStatusError,
Error: err.Error(),
})
}
func (a *API) writeErrorNoLog(ctx *gin.Context, status int, err error) {
ctx.AbortWithStatusJSON(status, &defs.APIError{
Status: defs.APIErrorStatusError,
Error: err.Error(),
})
@@ -238,10 +246,7 @@ func (a *API) middlewareAuth(ctx *gin.Context) {
if err != nil {
if err.AskCredentials {
ctx.Header("WWW-Authenticate", `Basic realm="mediamtx"`)
ctx.AbortWithStatusJSON(http.StatusUnauthorized, &defs.APIError{
Status: defs.APIErrorStatusError,
Error: "authentication error",
})
a.writeErrorNoLog(ctx, http.StatusUnauthorized, fmt.Errorf("authentication error"))
return
}
@@ -250,10 +255,7 @@ func (a *API) middlewareAuth(ctx *gin.Context) {
// wait some seconds to delay brute force attacks
<-time.After(auth.PauseAfterError)
ctx.AbortWithStatusJSON(http.StatusUnauthorized, &defs.APIError{
Status: defs.APIErrorStatusError,
Error: "authentication error",
})
a.writeErrorNoLog(ctx, http.StatusUnauthorized, fmt.Errorf("authentication error"))
return
}
}
+10 -8
View File
@@ -2,6 +2,7 @@
package metrics //nolint:revive
import (
"fmt"
"io"
"net"
"net/http"
@@ -154,6 +155,13 @@ func (m *Metrics) middlewarePreflightRequests(ctx *gin.Context) {
}
}
func (m *Metrics) writeErrorNoLog(ctx *gin.Context, status int, err error) {
ctx.AbortWithStatusJSON(status, &defs.APIError{
Status: defs.APIErrorStatusError,
Error: err.Error(),
})
}
func (m *Metrics) middlewareAuth(ctx *gin.Context) {
req := &auth.Request{
Action: conf.AuthActionMetrics,
@@ -166,10 +174,7 @@ func (m *Metrics) middlewareAuth(ctx *gin.Context) {
if err != nil {
if err.AskCredentials {
ctx.Header("WWW-Authenticate", `Basic realm="mediamtx"`)
ctx.AbortWithStatusJSON(http.StatusUnauthorized, &defs.APIError{
Status: defs.APIErrorStatusError,
Error: "authentication error",
})
m.writeErrorNoLog(ctx, http.StatusUnauthorized, fmt.Errorf("authentication error"))
return
}
@@ -178,10 +183,7 @@ func (m *Metrics) middlewareAuth(ctx *gin.Context) {
// wait some seconds to delay brute force attacks
<-time.After(auth.PauseAfterError)
ctx.AbortWithStatusJSON(http.StatusUnauthorized, &defs.APIError{
Status: defs.APIErrorStatusError,
Error: "authentication error",
})
m.writeErrorNoLog(ctx, http.StatusUnauthorized, fmt.Errorf("authentication error"))
return
}
}
+10 -8
View File
@@ -2,6 +2,7 @@
package playback
import (
"fmt"
"net"
"net/http"
"sync"
@@ -103,6 +104,13 @@ func (s *Server) writeError(ctx *gin.Context, status int, err error) {
ctx.String(status, err.Error())
}
func (s *Server) writeErrorNoLog(ctx *gin.Context, status int, err error) {
ctx.AbortWithStatusJSON(status, &defs.APIError{
Status: defs.APIErrorStatusError,
Error: err.Error(),
})
}
func (s *Server) safeFindPathConf(name string) (*conf.Path, error) {
s.mutex.RLock()
defer s.mutex.RUnlock()
@@ -134,10 +142,7 @@ func (s *Server) doAuth(ctx *gin.Context, pathName string) bool {
if err != nil {
if err.AskCredentials {
ctx.Header("WWW-Authenticate", `Basic realm="mediamtx"`)
ctx.AbortWithStatusJSON(http.StatusUnauthorized, &defs.APIError{
Status: defs.APIErrorStatusError,
Error: "authentication error",
})
s.writeErrorNoLog(ctx, http.StatusUnauthorized, fmt.Errorf("authentication error"))
return false
}
@@ -147,10 +152,7 @@ func (s *Server) doAuth(ctx *gin.Context, pathName string) bool {
// wait some seconds to delay brute force attacks
<-time.After(auth.PauseAfterError)
ctx.AbortWithStatusJSON(http.StatusUnauthorized, &defs.APIError{
Status: defs.APIErrorStatusError,
Error: "authentication error",
})
s.writeErrorNoLog(ctx, http.StatusUnauthorized, fmt.Errorf("authentication error"))
return false
}
+10 -8
View File
@@ -2,6 +2,7 @@
package pprof //nolint:revive
import (
"fmt"
"net"
"net/http"
"time"
@@ -101,6 +102,13 @@ func (pp *PPROF) middlewarePreflightRequests(ctx *gin.Context) {
}
}
func (pp *PPROF) writeErrorNoLog(ctx *gin.Context, status int, err error) {
ctx.AbortWithStatusJSON(status, &defs.APIError{
Status: defs.APIErrorStatusError,
Error: err.Error(),
})
}
func (pp *PPROF) middlewareAuth(ctx *gin.Context) {
req := &auth.Request{
Action: conf.AuthActionPprof,
@@ -113,10 +121,7 @@ func (pp *PPROF) middlewareAuth(ctx *gin.Context) {
if err != nil {
if err.AskCredentials {
ctx.Header("WWW-Authenticate", `Basic realm="mediamtx"`)
ctx.AbortWithStatusJSON(http.StatusUnauthorized, &defs.APIError{
Status: defs.APIErrorStatusError,
Error: "authentication error",
})
pp.writeErrorNoLog(ctx, http.StatusUnauthorized, fmt.Errorf("authentication error"))
return
}
@@ -125,10 +130,7 @@ func (pp *PPROF) middlewareAuth(ctx *gin.Context) {
// wait some seconds to delay brute force attacks
<-time.After(auth.PauseAfterError)
ctx.AbortWithStatusJSON(http.StatusUnauthorized, &defs.APIError{
Status: defs.APIErrorStatusError,
Error: "authentication error",
})
pp.writeErrorNoLog(ctx, http.StatusUnauthorized, fmt.Errorf("authentication error"))
return
}
}
+11 -9
View File
@@ -3,6 +3,7 @@ package hls
import (
_ "embed"
"errors"
"fmt"
"net"
"net/http"
gopath "path"
@@ -105,6 +106,13 @@ func (s *httpServer) middlewarePreflightRequests(ctx *gin.Context) {
}
}
func (s *httpServer) writeErrorNoLog(ctx *gin.Context, status int, err error) {
ctx.AbortWithStatusJSON(status, &defs.APIError{
Status: defs.APIErrorStatusError,
Error: err.Error(),
})
}
func (s *httpServer) onRequest(ctx *gin.Context) {
if ctx.Request.Method != http.MethodGet {
return
@@ -167,10 +175,7 @@ func (s *httpServer) onRequest(ctx *gin.Context) {
if errors.As(err, &terr) {
if terr.AskCredentials {
ctx.Header("WWW-Authenticate", `Basic realm="mediamtx"`)
ctx.AbortWithStatusJSON(http.StatusUnauthorized, &defs.APIError{
Status: defs.APIErrorStatusError,
Error: "authentication error",
})
s.writeErrorNoLog(ctx, http.StatusUnauthorized, fmt.Errorf("authentication error"))
return
}
@@ -179,14 +184,11 @@ func (s *httpServer) onRequest(ctx *gin.Context) {
// wait some seconds to delay brute force attacks
<-time.After(auth.PauseAfterError)
ctx.AbortWithStatusJSON(http.StatusUnauthorized, &defs.APIError{
Status: defs.APIErrorStatusError,
Error: "authentication error",
})
s.writeErrorNoLog(ctx, http.StatusUnauthorized, fmt.Errorf("authentication error"))
return
}
ctx.Writer.WriteHeader(http.StatusNotFound)
s.writeErrorNoLog(ctx, http.StatusInternalServerError, err)
return
}
+42
View File
@@ -1,6 +1,7 @@
package hls
import (
"encoding/json"
"fmt"
"io"
"net/http"
@@ -103,6 +104,47 @@ func TestServerPreflightRequest(t *testing.T) {
require.Equal(t, byts, []byte{})
}
func TestServerIndexNotConfigured(t *testing.T) {
pm := &dummyPathManager{
findPathConfImpl: func(req defs.PathFindPathConfReq) (*defs.PathFindPathConfRes, error) {
require.Equal(t, "nonconfigured", req.AccessRequest.Name)
return nil, fmt.Errorf("path is not configured")
},
}
s := &Server{
Address: "127.0.0.1:8888",
ReadTimeout: conf.Duration(10 * time.Second),
WriteTimeout: conf.Duration(10 * time.Second),
PathManager: pm,
Parent: test.NilLogger,
}
err := s.Initialize()
require.NoError(t, err)
defer s.Close()
req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8888/nonconfigured/", nil)
require.NoError(t, err)
res, err := http.DefaultClient.Do(req)
require.NoError(t, err)
defer res.Body.Close()
require.Equal(t, http.StatusInternalServerError, res.StatusCode)
require.Contains(t, res.Header.Get("Content-Type"), "application/json")
byts, err := io.ReadAll(res.Body)
require.NoError(t, err)
var payload defs.APIError
err = json.Unmarshal(byts, &payload)
require.NoError(t, err)
require.Equal(t, defs.APIError{
Status: defs.APIErrorStatusError,
Error: "path is not configured",
}, payload)
}
func TestServerNotFound(t *testing.T) {
for _, ca := range []string{
"always remux off",
+25 -34
View File
@@ -47,13 +47,6 @@ func mergePathAndQuery(path string, rawQuery string) string {
return res
}
func writeError(ctx *gin.Context, statusCode int, err error) {
ctx.JSON(statusCode, &defs.APIError{
Status: defs.APIErrorStatusError,
Error: err.Error(),
})
}
func sessionLocation(publish bool, path string, rawQuery string, secret uuid.UUID) string {
ret := "/" + path + "/"
@@ -133,6 +126,13 @@ func (s *httpServer) close() {
s.inner.Close()
}
func (s *httpServer) writeErrorNoLog(ctx *gin.Context, status int, err error) {
ctx.AbortWithStatusJSON(status, &defs.APIError{
Status: defs.APIErrorStatusError,
Error: err.Error(),
})
}
func (s *httpServer) checkAuthOutsideSession(ctx *gin.Context, pathName string, publish bool) bool {
_, err := s.pathManager.FindPathConf(defs.PathFindPathConfReq{
AccessRequest: defs.PathAccessRequest{
@@ -149,10 +149,7 @@ func (s *httpServer) checkAuthOutsideSession(ctx *gin.Context, pathName string,
if errors.As(err, &terr) {
if terr.AskCredentials {
ctx.Header("WWW-Authenticate", `Basic realm="mediamtx"`)
ctx.AbortWithStatusJSON(http.StatusUnauthorized, &defs.APIError{
Status: defs.APIErrorStatusError,
Error: "authentication error",
})
s.writeErrorNoLog(ctx, http.StatusUnauthorized, fmt.Errorf("authentication error"))
return false
}
@@ -161,11 +158,11 @@ func (s *httpServer) checkAuthOutsideSession(ctx *gin.Context, pathName string,
// wait some seconds to delay brute force attacks
<-time.After(auth.PauseAfterError)
writeError(ctx, http.StatusUnauthorized, terr)
s.writeErrorNoLog(ctx, http.StatusUnauthorized, fmt.Errorf("authentication error"))
return false
}
writeError(ctx, http.StatusInternalServerError, err)
s.writeErrorNoLog(ctx, http.StatusInternalServerError, err)
return false
}
@@ -179,7 +176,7 @@ func (s *httpServer) onWHIPOptions(ctx *gin.Context, pathName string, publish bo
servers, err := s.parent.generateICEServers(true)
if err != nil {
writeError(ctx, http.StatusInternalServerError, err)
s.writeErrorNoLog(ctx, http.StatusInternalServerError, err)
return
}
@@ -193,7 +190,7 @@ func (s *httpServer) onWHIPOptions(ctx *gin.Context, pathName string, publish bo
func (s *httpServer) onWHIPPost(ctx *gin.Context, pathName string, publish bool) {
contentType := httpp.ParseContentType(ctx.Request.Header.Get("Content-Type"))
if contentType != "application/sdp" {
writeError(ctx, http.StatusBadRequest, fmt.Errorf("invalid Content-Type"))
s.writeErrorNoLog(ctx, http.StatusBadRequest, fmt.Errorf("invalid Content-Type"))
return
}
@@ -214,10 +211,7 @@ func (s *httpServer) onWHIPPost(ctx *gin.Context, pathName string, publish bool)
if errors.As(res.err, &terr) {
if terr.AskCredentials {
ctx.Header("WWW-Authenticate", `Basic realm="mediamtx"`)
ctx.AbortWithStatusJSON(http.StatusUnauthorized, &defs.APIError{
Status: defs.APIErrorStatusError,
Error: "authentication error",
})
s.writeErrorNoLog(ctx, http.StatusUnauthorized, fmt.Errorf("authentication error"))
return
}
@@ -226,20 +220,17 @@ func (s *httpServer) onWHIPPost(ctx *gin.Context, pathName string, publish bool)
// wait some seconds to delay brute force attacks
<-time.After(auth.PauseAfterError)
ctx.AbortWithStatusJSON(http.StatusUnauthorized, &defs.APIError{
Status: defs.APIErrorStatusError,
Error: "authentication error",
})
s.writeErrorNoLog(ctx, http.StatusUnauthorized, fmt.Errorf("authentication error"))
return
}
writeError(ctx, res.errStatusCode, res.err)
s.writeErrorNoLog(ctx, res.errStatusCode, res.err)
return
}
servers, err := s.parent.generateICEServers(true)
if err != nil {
writeError(ctx, http.StatusInternalServerError, err)
s.writeErrorNoLog(ctx, http.StatusInternalServerError, err)
return
}
@@ -259,13 +250,13 @@ func (s *httpServer) onWHIPPost(ctx *gin.Context, pathName string, publish bool)
func (s *httpServer) onWHIPPatch(ctx *gin.Context, pathName string, rawSecret string) {
secret, err := uuid.Parse(rawSecret)
if err != nil {
writeError(ctx, http.StatusBadRequest, fmt.Errorf("invalid secret"))
s.writeErrorNoLog(ctx, http.StatusBadRequest, fmt.Errorf("invalid secret"))
return
}
contentType := httpp.ParseContentType(ctx.Request.Header.Get("Content-Type"))
if contentType != "application/trickle-ice-sdpfrag" {
writeError(ctx, http.StatusBadRequest, fmt.Errorf("invalid Content-Type"))
s.writeErrorNoLog(ctx, http.StatusBadRequest, fmt.Errorf("invalid Content-Type"))
return
}
@@ -276,7 +267,7 @@ func (s *httpServer) onWHIPPatch(ctx *gin.Context, pathName string, rawSecret st
candidates, err := whip.ICEFragmentUnmarshal(byts)
if err != nil {
writeError(ctx, http.StatusBadRequest, err)
s.writeErrorNoLog(ctx, http.StatusBadRequest, err)
return
}
@@ -287,9 +278,9 @@ func (s *httpServer) onWHIPPatch(ctx *gin.Context, pathName string, rawSecret st
})
if res.err != nil {
if errors.Is(res.err, ErrSessionNotFound) {
writeError(ctx, http.StatusNotFound, res.err)
s.writeErrorNoLog(ctx, http.StatusNotFound, res.err)
} else {
writeError(ctx, http.StatusInternalServerError, res.err)
s.writeErrorNoLog(ctx, http.StatusInternalServerError, res.err)
}
return
}
@@ -302,7 +293,7 @@ func (s *httpServer) onWHIPPatch(ctx *gin.Context, pathName string, rawSecret st
func (s *httpServer) onWHIPDelete(ctx *gin.Context, pathName string, rawSecret string) {
secret, err := uuid.Parse(rawSecret)
if err != nil {
writeError(ctx, http.StatusBadRequest, fmt.Errorf("invalid secret"))
s.writeErrorNoLog(ctx, http.StatusBadRequest, fmt.Errorf("invalid secret"))
return
}
@@ -312,9 +303,9 @@ func (s *httpServer) onWHIPDelete(ctx *gin.Context, pathName string, rawSecret s
})
if err != nil {
if errors.Is(err, ErrSessionNotFound) {
writeError(ctx, http.StatusNotFound, err)
s.writeErrorNoLog(ctx, http.StatusNotFound, err)
} else {
writeError(ctx, http.StatusInternalServerError, err)
s.writeErrorNoLog(ctx, http.StatusInternalServerError, err)
}
return
}
@@ -384,7 +375,7 @@ func (s *httpServer) onRequest(ctx *gin.Context) {
// RFC draft-ietf-whip-09
// The WHIP endpoints MUST return an "405 Method Not Allowed" response
// for any HTTP GET, HEAD or PUT requests
writeError(ctx, http.StatusMethodNotAllowed, fmt.Errorf("method not allowed"))
s.writeErrorNoLog(ctx, http.StatusMethodNotAllowed, fmt.Errorf("method not allowed"))
}
return
}
+51
View File
@@ -3,6 +3,7 @@ package webrtc
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
@@ -142,6 +143,56 @@ func TestPreflightRequest(t *testing.T) {
require.Equal(t, byts, []byte{})
}
func TestServerIndexNotConfigured(t *testing.T) {
pathManager := &test.PathManager{
FindPathConfImpl: func(req defs.PathFindPathConfReq) (*defs.PathFindPathConfRes, error) {
require.Equal(t, "nonconfigured", req.AccessRequest.Name)
return nil, fmt.Errorf("path is not configured")
},
}
s := &Server{
Address: "127.0.0.1:8886",
ReadTimeout: conf.Duration(10 * time.Second),
WriteTimeout: conf.Duration(10 * time.Second),
LocalUDPAddress: "127.0.0.1:8887",
LocalTCPAddress: "127.0.0.1:8887",
IPsFromInterfaces: true,
IPsFromInterfacesList: []string{},
AdditionalHosts: []string{},
ICEServers: []conf.WebRTCICEServer{},
STUNGatherTimeout: conf.Duration(5 * time.Second),
HandshakeTimeout: conf.Duration(10 * time.Second),
TrackGatherTimeout: conf.Duration(2 * time.Second),
PathManager: pathManager,
Parent: test.NilLogger,
}
err := s.Initialize()
require.NoError(t, err)
defer s.Close()
req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8886/nonconfigured/", nil)
require.NoError(t, err)
res, err := http.DefaultClient.Do(req)
require.NoError(t, err)
defer res.Body.Close()
require.Equal(t, http.StatusInternalServerError, res.StatusCode)
require.Contains(t, res.Header.Get("Content-Type"), "application/json")
byts, err := io.ReadAll(res.Body)
require.NoError(t, err)
var payload defs.APIError
err = json.Unmarshal(byts, &payload)
require.NoError(t, err)
require.Equal(t, defs.APIError{
Status: defs.APIErrorStatusError,
Error: "path is not configured",
}, payload)
}
func TestServerOptionsICEServer(t *testing.T) {
pathManager := &test.PathManager{
FindPathConfImpl: func(req defs.PathFindPathConfReq) (*defs.PathFindPathConfRes, error) {