support using JWT in Authorization header with API, Metrics, PProf (#3630) (#3795)

This commit is contained in:
Alessandro Ros
2024-10-05 21:15:21 +02:00
committed by GitHub
parent 4b9d3ceb89
commit 534b637bc7
24 changed files with 275 additions and 515 deletions

View File

@@ -1188,7 +1188,7 @@ The JWT is expected to contain a claim, with a list of permissions in the same f
}
```
Clients are expected to pass the JWT in the Authorization header (in case of HLS and WebRTC) or in query parameters (in case of all other protocols), for instance:
Clients are expected to pass the JWT in the Authorization header (in case of HLS, WebRTC and all web-based features) or in query parameters (in case of all other protocols), for instance:
```
ffmpeg -re -stream_loop -1 -i file.ts -c copy -f rtsp rtsp://localhost:8554/mystream?jwt=MY_JWT

View File

@@ -284,17 +284,13 @@ func (a *API) middlewareOrigin(ctx *gin.Context) {
}
func (a *API) middlewareAuth(ctx *gin.Context) {
user, pass, hasCredentials := ctx.Request.BasicAuth()
err := a.AuthManager.Authenticate(&auth.Request{
User: user,
Pass: pass,
Query: ctx.Request.URL.RawQuery,
IP: net.ParseIP(ctx.ClientIP()),
Action: conf.AuthActionAPI,
HTTPRequest: ctx.Request,
})
if err != nil {
if !hasCredentials {
if err.(*auth.Error).AskCredentials { //nolint:errorlint
ctx.Header("WWW-Authenticate", `Basic realm="mediamtx"`)
ctx.AbortWithStatus(http.StatusUnauthorized)
return

View File

@@ -11,7 +11,6 @@ import (
"testing"
"time"
"github.com/bluenviron/mediamtx/internal/auth"
"github.com/bluenviron/mediamtx/internal/conf"
"github.com/bluenviron/mediamtx/internal/logger"
"github.com/bluenviron/mediamtx/internal/test"
@@ -111,40 +110,6 @@ func TestPreflightRequest(t *testing.T) {
require.Equal(t, byts, []byte{})
}
func TestConfigAuth(t *testing.T) {
cnf := tempConf(t, "api: yes\n")
api := API{
Address: "localhost:9997",
ReadTimeout: conf.StringDuration(10 * time.Second),
Conf: cnf,
AuthManager: &test.AuthManager{
Func: func(req *auth.Request) error {
require.Equal(t, &auth.Request{
User: "myuser",
Pass: "mypass",
IP: req.IP,
Action: "api",
Query: "key=val",
}, req)
return nil
},
},
Parent: &testParent{},
}
err := api.Initialize()
require.NoError(t, err)
defer api.Close()
tr := &http.Transport{}
defer tr.CloseIdleConnections()
hc := &http.Client{Transport: tr}
var out map[string]interface{}
httpRequest(t, hc, http.MethodGet, "http://myuser:mypass@localhost:9997/v3/config/global/get?key=val", nil, &out)
require.Equal(t, true, out["api"])
}
func TestConfigGlobalGet(t *testing.T) {
cnf := tempConf(t, "api: yes\n")

View File

@@ -31,6 +31,17 @@ const (
jwtRefreshPeriod = 60 * 60 * time.Second
)
func addJWTFromAuthorization(rawQuery string, auth string) string {
jwt := strings.TrimPrefix(auth, "Bearer ")
if rawQuery != "" {
if v, err := url.ParseQuery(rawQuery); err == nil && v.Get("jwt") == "" {
v.Set("jwt", jwt)
return v.Encode()
}
}
return url.Values{"jwt": []string{jwt}}.Encode()
}
// Protocol is a protocol.
type Protocol string
@@ -55,17 +66,23 @@ type Request struct {
Protocol Protocol
ID *uuid.UUID
Query string
// RTSP only
RTSPRequest *base.Request
RTSPNonce string
// HTTP only
HTTPRequest *http.Request
}
// Error is a authentication error.
type Error struct {
Message string
AskCredentials bool
}
// Error implements the error interface.
func (e Error) Error() string {
func (e *Error) Error() string {
return "authentication failed: " + e.Message
}
@@ -154,15 +171,6 @@ func (m *Manager) ReloadInternalUsers(u []conf.AuthInternalUser) {
// Authenticate authenticates a request.
func (m *Manager) Authenticate(req *Request) error {
err := m.authenticateInner(req)
if err != nil {
return Error{Message: err.Error()}
}
return nil
}
func (m *Manager) authenticateInner(req *Request) error {
// if this is a RTSP request, fill username and password
var rtspAuthHeader headers.Authorization
if req.RTSPRequest != nil {
@@ -175,18 +183,42 @@ func (m *Manager) authenticateInner(req *Request) error {
req.User = rtspAuthHeader.Username
}
}
} else if req.HTTPRequest != nil {
req.User, req.Pass, _ = req.HTTPRequest.BasicAuth()
req.Query = req.HTTPRequest.URL.RawQuery
if h := req.HTTPRequest.Header.Get("Authorization"); strings.HasPrefix(h, "Bearer ") {
// support passing username and password through Authorization header
if parts := strings.Split(strings.TrimPrefix(h, "Bearer "), ":"); len(parts) == 2 {
req.User = parts[0]
req.Pass = parts[1]
} else {
req.Query = addJWTFromAuthorization(req.Query, h)
}
}
}
var err error
switch m.Method {
case conf.AuthMethodInternal:
return m.authenticateInternal(req, &rtspAuthHeader)
err = m.authenticateInternal(req, &rtspAuthHeader)
case conf.AuthMethodHTTP:
return m.authenticateHTTP(req)
err = m.authenticateHTTP(req)
default:
return m.authenticateJWT(req)
err = m.authenticateJWT(req)
}
if err != nil {
return &Error{
Message: err.Error(),
AskCredentials: (req.User == "" && req.Pass == ""),
}
}
return nil
}
func (m *Manager) authenticateInternal(req *Request, rtspAuthHeader *headers.Authorization) error {

View File

@@ -7,6 +7,7 @@ import (
"encoding/json"
"net"
"net/http"
"net/url"
"testing"
"time"
@@ -186,6 +187,37 @@ func TestAuthInternalRTSPDigest(t *testing.T) {
require.NoError(t, err)
}
func TestAuthInternalCredentialsInBearer(t *testing.T) {
m := Manager{
Method: conf.AuthMethodInternal,
InternalUsers: []conf.AuthInternalUser{
{
User: "myuser",
Pass: "mypass",
IPs: conf.IPNetworks{mustParseCIDR("127.1.1.1/32")},
Permissions: []conf.AuthInternalUserPermission{{
Action: conf.AuthActionPublish,
Path: "mypath",
}},
},
},
HTTPAddress: "",
RTSPAuthMethods: []auth.ValidateMethod{auth.ValidateMethodDigestMD5},
}
err := m.Authenticate(&Request{
IP: net.ParseIP("127.1.1.1"),
Action: conf.AuthActionPublish,
Path: "mypath",
Protocol: ProtocolRTSP,
HTTPRequest: &http.Request{
Header: http.Header{"Authorization": []string{"Bearer myuser:mypass"}},
URL: &url.URL{},
},
})
require.NoError(t, err)
}
func TestAuthHTTP(t *testing.T) {
for _, outcome := range []string{"ok", "fail"} {
t.Run(outcome, func(t *testing.T) {
@@ -292,6 +324,8 @@ func TestAuthJWT(t *testing.T) {
// taken from
// https://github.com/MicahParks/jwkset/blob/master/examples/http_server/main.go
for _, ca := range []string{"query", "auth header"} {
t.Run(ca, func(t *testing.T) {
key, err := rsa.GenerateKey(rand.Reader, 1024)
require.NoError(t, err)
@@ -356,14 +390,27 @@ func TestAuthJWT(t *testing.T) {
JWTClaimKey: "my_permission_key",
}
if ca == "query" {
err = m.Authenticate(&Request{
User: "",
Pass: "",
IP: net.ParseIP("127.0.0.1"),
Action: conf.AuthActionPublish,
Path: "mypath",
Protocol: ProtocolRTSP,
Query: "param=value&jwt=" + ss,
})
} else {
err = m.Authenticate(&Request{
IP: net.ParseIP("127.0.0.1"),
Action: conf.AuthActionPublish,
Path: "mypath",
Protocol: ProtocolWebRTC,
HTTPRequest: &http.Request{
Header: http.Header{"Authorization": []string{"Bearer " + ss}},
URL: &url.URL{},
},
})
}
require.NoError(t, err)
})
}
}

View File

@@ -3,6 +3,7 @@ package defs
import (
"fmt"
"net"
"net/http"
"github.com/bluenviron/gortsplib/v4/pkg/base"
"github.com/bluenviron/gortsplib/v4/pkg/description"
@@ -35,7 +36,7 @@ type Path interface {
RemoveReader(req PathRemoveReaderReq)
}
// PathAccessRequest is an access request.
// PathAccessRequest is a path access request.
type PathAccessRequest struct {
Name string
Query string
@@ -43,13 +44,18 @@ type PathAccessRequest struct {
SkipAuth bool
// only if skipAuth = false
IP net.IP
User string
Pass string
IP net.IP
Proto auth.Protocol
ID *uuid.UUID
// RTSP only
RTSPRequest *base.Request
RTSPNonce string
// HTTP only
HTTPRequest *http.Request
}
// ToAuthRequest converts a path access request into an authentication request.
@@ -70,6 +76,7 @@ func (r *PathAccessRequest) ToAuthRequest() *auth.Request {
Query: r.Query,
RTSPRequest: r.RTSPRequest,
RTSPNonce: r.RTSPNonce,
HTTPRequest: r.HTTPRequest,
}
}

View File

@@ -120,17 +120,13 @@ func (m *Metrics) onRequest(ctx *gin.Context) {
return
}
user, pass, hasCredentials := ctx.Request.BasicAuth()
err := m.AuthManager.Authenticate(&auth.Request{
User: user,
Pass: pass,
Query: ctx.Request.URL.RawQuery,
IP: net.ParseIP(ctx.ClientIP()),
Action: conf.AuthActionMetrics,
HTTPRequest: ctx.Request,
})
if err != nil {
if !hasCredentials {
if err.(*auth.Error).AskCredentials { //nolint:errorlint
ctx.Header("WWW-Authenticate", `Basic realm="mediamtx"`)
ctx.AbortWithStatus(http.StatusUnauthorized)
return

View File

@@ -12,7 +12,6 @@ import (
"github.com/bluenviron/mediacommon/pkg/codecs/mpeg4audio"
"github.com/bluenviron/mediacommon/pkg/formats/fmp4"
"github.com/bluenviron/mediacommon/pkg/formats/fmp4/seekablebuffer"
"github.com/bluenviron/mediamtx/internal/auth"
"github.com/bluenviron/mediamtx/internal/conf"
"github.com/bluenviron/mediamtx/internal/test"
"github.com/stretchr/testify/require"
@@ -239,19 +238,7 @@ func TestOnGet(t *testing.T) {
RecordPath: filepath.Join(dir, "%path/%Y-%m-%d_%H-%M-%S-%f"),
},
},
AuthManager: &test.AuthManager{
Func: func(req *auth.Request) error {
require.Equal(t, &auth.Request{
User: "myuser",
Pass: "mypass",
IP: req.IP,
Action: "playback",
Path: "mypath",
Query: req.Query,
}, req)
return nil
},
},
AuthManager: test.NilAuthManager,
Parent: test.NilLogger,
}
err = s.Initialize()

View File

@@ -9,7 +9,6 @@ import (
"testing"
"time"
"github.com/bluenviron/mediamtx/internal/auth"
"github.com/bluenviron/mediamtx/internal/conf"
"github.com/bluenviron/mediamtx/internal/test"
"github.com/stretchr/testify/require"
@@ -36,19 +35,7 @@ func TestOnList(t *testing.T) {
RecordPath: filepath.Join(dir, "%path/%Y-%m-%d_%H-%M-%S-%f"),
},
},
AuthManager: &test.AuthManager{
Func: func(req *auth.Request) error {
require.Equal(t, &auth.Request{
User: "myuser",
Pass: "mypass",
IP: req.IP,
Action: "playback",
Query: "path=mypath",
Path: "mypath",
}, req)
return nil
},
},
AuthManager: test.NilAuthManager,
Parent: test.NilLogger,
}
err = s.Initialize()

View File

@@ -2,7 +2,6 @@
package playback
import (
"errors"
"net"
"net/http"
"sync"
@@ -119,27 +118,21 @@ func (s *Server) middlewareOrigin(ctx *gin.Context) {
}
func (s *Server) doAuth(ctx *gin.Context, pathName string) bool {
user, pass, hasCredentials := ctx.Request.BasicAuth()
err := s.AuthManager.Authenticate(&auth.Request{
User: user,
Pass: pass,
Query: ctx.Request.URL.RawQuery,
IP: net.ParseIP(ctx.ClientIP()),
Action: conf.AuthActionPlayback,
Path: pathName,
HTTPRequest: ctx.Request,
})
if err != nil {
if !hasCredentials {
if err.(*auth.Error).AskCredentials { //nolint:errorlint
ctx.Header("WWW-Authenticate", `Basic realm="mediamtx"`)
ctx.Writer.WriteHeader(http.StatusUnauthorized)
return false
}
var terr auth.Error
errors.As(err, &terr)
s.Log(logger.Info, "connection %v failed to authenticate: %v", httpp.RemoteAddr(ctx), terr.Message)
s.Log(logger.Info, "connection %v failed to authenticate: %v",
httpp.RemoteAddr(ctx), err.(*auth.Error).Message) //nolint:errorlint
// wait some seconds to mitigate brute force attacks
<-time.After(auth.PauseAfterError)

View File

@@ -92,17 +92,13 @@ func (pp *PPROF) onRequest(ctx *gin.Context) {
return
}
user, pass, hasCredentials := ctx.Request.BasicAuth()
err := pp.AuthManager.Authenticate(&auth.Request{
User: user,
Pass: pass,
Query: ctx.Request.URL.RawQuery,
IP: net.ParseIP(ctx.ClientIP()),
Action: conf.AuthActionMetrics,
HTTPRequest: ctx.Request,
})
if err != nil {
if !hasCredentials {
if err.(*auth.Error).AskCredentials { //nolint:errorlint
ctx.Writer.Header().Set("WWW-Authenticate", `Basic realm="mediamtx"`)
ctx.Writer.WriteHeader(http.StatusUnauthorized)
return

View File

@@ -5,7 +5,6 @@ import (
"errors"
"net"
"net/http"
"net/url"
gopath "path"
"strings"
"time"
@@ -37,17 +36,6 @@ func mergePathAndQuery(path string, rawQuery string) string {
return res
}
func addJWTFromAuthorization(rawQuery string, auth string) string {
jwt := strings.TrimPrefix(auth, "Bearer ")
if rawQuery != "" {
if v, err := url.ParseQuery(rawQuery); err == nil && v.Get("jwt") == "" {
v.Set("jwt", jwt)
return v.Encode()
}
}
return url.Values{"jwt": []string{jwt}}.Encode()
}
type httpServer struct {
address string
encryption bool
@@ -157,28 +145,19 @@ func (s *httpServer) onRequest(ctx *gin.Context) {
return
}
user, pass, hasCredentials := ctx.Request.BasicAuth()
q := ctx.Request.URL.RawQuery
if h := ctx.Request.Header.Get("Authorization"); strings.HasPrefix(h, "Bearer ") {
q = addJWTFromAuthorization(q, h)
}
pathConf, err := s.pathManager.FindPathConf(defs.PathFindPathConfReq{
AccessRequest: defs.PathAccessRequest{
Name: dir,
Query: q,
Publish: false,
IP: net.ParseIP(ctx.ClientIP()),
User: user,
Pass: pass,
Proto: auth.ProtocolHLS,
HTTPRequest: ctx.Request,
},
})
if err != nil {
var terr auth.Error
var terr *auth.Error
if errors.As(err, &terr) {
if !hasCredentials {
if terr.AskCredentials {
ctx.Header("WWW-Authenticate", `Basic realm="mediamtx"`)
ctx.Writer.WriteHeader(http.StatusUnauthorized)
return

View File

@@ -106,8 +106,6 @@ func TestServerNotFound(t *testing.T) {
pm := &dummyPathManager{
findPathConf: func(req defs.PathFindPathConfReq) (*conf.Path, error) {
require.Equal(t, "nonexisting", req.AccessRequest.Name)
require.Equal(t, "myuser", req.AccessRequest.User)
require.Equal(t, "mypass", req.AccessRequest.Pass)
return &conf.Path{}, nil
},
addReader: func(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) {
@@ -181,8 +179,6 @@ func TestServerRead(t *testing.T) {
pm := &dummyPathManager{
findPathConf: func(req defs.PathFindPathConfReq) (*conf.Path, error) {
require.Equal(t, "mystream", req.AccessRequest.Name)
require.Equal(t, "myuser", req.AccessRequest.User)
require.Equal(t, "mypass", req.AccessRequest.Pass)
return &conf.Path{}, nil
},
addReader: func(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) {
@@ -277,8 +273,6 @@ func TestServerRead(t *testing.T) {
pm := &dummyPathManager{
findPathConf: func(req defs.PathFindPathConfReq) (*conf.Path, error) {
require.Equal(t, "mystream", req.AccessRequest.Name)
require.Equal(t, "myuser", req.AccessRequest.User)
require.Equal(t, "mypass", req.AccessRequest.Pass)
return &conf.Path{}, nil
},
addReader: func(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) {
@@ -372,8 +366,7 @@ func TestServerReadAuthorizationHeader(t *testing.T) {
require.NoError(t, err)
pm := &dummyPathManager{
findPathConf: func(req defs.PathFindPathConfReq) (*conf.Path, error) {
require.Equal(t, "jwt=testing", req.AccessRequest.Query)
findPathConf: func(_ defs.PathFindPathConfReq) (*conf.Path, error) {
return &conf.Path{}, nil
},
addReader: func(_ defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) {

View File

@@ -168,7 +168,7 @@ func (c *conn) runRead(conn *rtmp.Conn, u *url.URL) error {
},
})
if err != nil {
var terr auth.Error
var terr *auth.Error
if errors.As(err, &terr) {
// wait some seconds to mitigate brute force attacks
<-time.After(auth.PauseAfterError)
@@ -235,7 +235,7 @@ func (c *conn) runPublish(conn *rtmp.Conn, u *url.URL) error {
},
})
if err != nil {
var terr auth.Error
var terr *auth.Error
if errors.As(err, &terr) {
// wait some seconds to mitigate brute force attacks
<-time.After(auth.PauseAfterError)

View File

@@ -68,14 +68,14 @@ type dummyPathManager struct {
func (pm *dummyPathManager) AddPublisher(req defs.PathAddPublisherReq) (defs.Path, error) {
if req.AccessRequest.User != "myuser" || req.AccessRequest.Pass != "mypass" {
return nil, auth.Error{}
return nil, &auth.Error{}
}
return pm.path, nil
}
func (pm *dummyPathManager) AddReader(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) {
if req.AccessRequest.User != "myuser" || req.AccessRequest.Pass != "mypass" {
return nil, nil, auth.Error{}
return nil, nil, &auth.Error{}
}
return pm.path, pm.path.stream, nil
}

View File

@@ -139,7 +139,7 @@ func (c *conn) onDescribe(ctx *gortsplib.ServerHandlerOnDescribeCtx,
})
if res.Err != nil {
var terr auth.Error
var terr *auth.Error
if errors.As(res.Err, &terr) {
res, err := c.handleAuthError(terr)
return res, nil, err

View File

@@ -125,7 +125,7 @@ func (s *session) onAnnounce(c *conn, ctx *gortsplib.ServerHandlerOnAnnounceCtx)
},
})
if err != nil {
var terr auth.Error
var terr *auth.Error
if errors.As(err, &terr) {
return c.handleAuthError(terr)
}
@@ -195,7 +195,7 @@ func (s *session) onSetup(c *conn, ctx *gortsplib.ServerHandlerOnSetupCtx,
},
})
if err != nil {
var terr auth.Error
var terr *auth.Error
if errors.As(err, &terr) {
res, err2 := c.handleAuthError(terr)
return res, nil, err2

View File

@@ -151,7 +151,7 @@ func (c *conn) runPublish(streamID *streamID) error {
},
})
if err != nil {
var terr auth.Error
var terr *auth.Error
if errors.As(err, &terr) {
// wait some seconds to mitigate brute force attacks
<-time.After(auth.PauseAfterError)
@@ -250,7 +250,7 @@ func (c *conn) runRead(streamID *streamID) error {
},
})
if err != nil {
var terr auth.Error
var terr *auth.Error
if errors.As(err, &terr) {
// wait some seconds to mitigate brute force attacks
<-time.After(auth.PauseAfterError)

View File

@@ -66,14 +66,14 @@ type dummyPathManager struct {
func (pm *dummyPathManager) AddPublisher(req defs.PathAddPublisherReq) (defs.Path, error) {
if req.AccessRequest.User != "myuser" || req.AccessRequest.Pass != "mypass" {
return nil, auth.Error{}
return nil, &auth.Error{}
}
return pm.path, nil
}
func (pm *dummyPathManager) AddReader(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) {
if req.AccessRequest.User != "myuser" || req.AccessRequest.Pass != "mypass" {
return nil, nil, auth.Error{}
return nil, nil, &auth.Error{}
}
return pm.path, pm.path.stream, nil
}

View File

@@ -7,7 +7,6 @@ import (
"io"
"net"
"net/http"
"net/url"
"regexp"
"strings"
"time"
@@ -60,17 +59,6 @@ func sessionLocation(publish bool, path string, secret uuid.UUID) string {
return ret
}
func addJWTFromAuthorization(rawQuery string, auth string) string {
jwt := strings.TrimPrefix(auth, "Bearer ")
if rawQuery != "" {
if v, err := url.ParseQuery(rawQuery); err == nil && v.Get("jwt") == "" {
v.Set("jwt", jwt)
return v.Encode()
}
}
return url.Values{"jwt": []string{jwt}}.Encode()
}
type httpServer struct {
address string
encryption bool
@@ -120,35 +108,19 @@ func (s *httpServer) close() {
}
func (s *httpServer) checkAuthOutsideSession(ctx *gin.Context, pathName string, publish bool) bool {
user, pass, hasCredentials := ctx.Request.BasicAuth()
q := ctx.Request.URL.RawQuery
if h := ctx.Request.Header.Get("Authorization"); strings.HasPrefix(h, "Bearer ") {
// JWT in authorization bearer -> JWT in query parameters
q = addJWTFromAuthorization(q, h)
// credentials in authorization bearer -> credentials in authorization basic
if parts := strings.Split(strings.TrimPrefix(h, "Bearer "), ":"); len(parts) == 2 {
user = parts[0]
pass = parts[1]
}
}
_, err := s.pathManager.FindPathConf(defs.PathFindPathConfReq{
AccessRequest: defs.PathAccessRequest{
Name: pathName,
Query: q,
Publish: publish,
IP: net.ParseIP(ctx.ClientIP()),
User: user,
Pass: pass,
Proto: auth.ProtocolWebRTC,
HTTPRequest: ctx.Request,
},
})
if err != nil {
var terr auth.Error
var terr *auth.Error
if errors.As(err, &terr) {
if !hasCredentials {
if terr.AskCredentials {
ctx.Header("WWW-Authenticate", `Basic realm="mediamtx"`)
ctx.Writer.WriteHeader(http.StatusUnauthorized)
return false
@@ -200,30 +172,31 @@ func (s *httpServer) onWHIPPost(ctx *gin.Context, pathName string, publish bool)
return
}
user, pass, _ := ctx.Request.BasicAuth()
q := ctx.Request.URL.RawQuery
if h := ctx.Request.Header.Get("Authorization"); strings.HasPrefix(h, "Bearer ") {
// JWT in authorization bearer -> JWT in query parameters
q = addJWTFromAuthorization(q, h)
// credentials in authorization bearer -> credentials in authorization basic
if parts := strings.Split(strings.TrimPrefix(h, "Bearer "), ":"); len(parts) == 2 {
user = parts[0]
pass = parts[1]
}
}
res := s.parent.newSession(webRTCNewSessionReq{
pathName: pathName,
remoteAddr: httpp.RemoteAddr(ctx),
query: q,
user: user,
pass: pass,
offer: offer,
publish: publish,
httpRequest: ctx.Request,
})
if res.err != nil {
var terr *auth.Error
if errors.As(err, &terr) {
if terr.AskCredentials {
ctx.Header("WWW-Authenticate", `Basic realm="mediamtx"`)
ctx.AbortWithStatus(http.StatusUnauthorized)
return
}
s.Log(logger.Info, "connection %v failed to authenticate: %v", httpp.RemoteAddr(ctx), terr.Message)
// wait some seconds to mitigate brute force attacks
<-time.After(auth.PauseAfterError)
writeError(ctx, http.StatusUnauthorized, terr)
return
}
writeError(ctx, res.errStatusCode, res.err)
return
}

View File

@@ -135,11 +135,9 @@ type webRTCNewSessionRes struct {
type webRTCNewSessionReq struct {
pathName string
remoteAddr string
query string
user string
pass string
offer []byte
publish bool
httpRequest *http.Request
res chan webRTCNewSessionRes
}

View File

@@ -96,9 +96,7 @@ func (pm *dummyPathManager) AddReader(req defs.PathAddReaderReq) (defs.Path, *st
func initializeTestServer(t *testing.T) *Server {
pm := &dummyPathManager{
findPathConf: func(req defs.PathFindPathConfReq) (*conf.Path, error) {
require.Equal(t, "myuser", req.AccessRequest.User)
require.Equal(t, "mypass", req.AccessRequest.Pass)
findPathConf: func(_ defs.PathFindPathConfReq) (*conf.Path, error) {
return &conf.Path{}, nil
},
}
@@ -182,9 +180,7 @@ func TestPreflightRequest(t *testing.T) {
func TestServerOptionsICEServer(t *testing.T) {
pathManager := &dummyPathManager{
findPathConf: func(req defs.PathFindPathConfReq) (*conf.Path, error) {
require.Equal(t, "myuser", req.AccessRequest.User)
require.Equal(t, "mypass", req.AccessRequest.Pass)
findPathConf: func(_ defs.PathFindPathConfReq) (*conf.Path, error) {
return &conf.Path{}, nil
},
}
@@ -249,14 +245,10 @@ func TestServerPublish(t *testing.T) {
pathManager := &dummyPathManager{
findPathConf: func(req defs.PathFindPathConfReq) (*conf.Path, error) {
require.Equal(t, "teststream", req.AccessRequest.Name)
require.Equal(t, "myuser", req.AccessRequest.User)
require.Equal(t, "mypass", req.AccessRequest.Pass)
return &conf.Path{}, nil
},
addPublisher: func(req defs.PathAddPublisherReq) (defs.Path, error) {
require.Equal(t, "teststream", req.AccessRequest.Name)
require.Equal(t, "myuser", req.AccessRequest.User)
require.Equal(t, "mypass", req.AccessRequest.Pass)
return path, nil
},
}
@@ -534,14 +526,10 @@ func TestServerRead(t *testing.T) {
pathManager := &dummyPathManager{
findPathConf: func(req defs.PathFindPathConfReq) (*conf.Path, error) {
require.Equal(t, "teststream", req.AccessRequest.Name)
require.Equal(t, "myuser", req.AccessRequest.User)
require.Equal(t, "mypass", req.AccessRequest.Pass)
return &conf.Path{}, nil
},
addReader: func(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) {
require.Equal(t, "teststream", req.AccessRequest.Name)
require.Equal(t, "myuser", req.AccessRequest.User)
require.Equal(t, "mypass", req.AccessRequest.Pass)
return path, str, nil
},
}
@@ -632,167 +620,9 @@ func TestServerRead(t *testing.T) {
}
}
func TestServerReadAuthorizationBearerJWT(t *testing.T) {
desc := &description.Session{Medias: []*description.Media{test.MediaH264}}
str, err := stream.New(
512,
1460,
desc,
true,
test.NilLogger,
)
require.NoError(t, err)
path := &dummyPath{stream: str}
pm := &dummyPathManager{
findPathConf: func(req defs.PathFindPathConfReq) (*conf.Path, error) {
require.Equal(t, "jwt=testing", req.AccessRequest.Query)
return &conf.Path{}, nil
},
addReader: func(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) {
require.Equal(t, "jwt=testing", req.AccessRequest.Query)
return path, str, nil
},
}
s := &Server{
Address: "127.0.0.1:8886",
Encryption: false,
ServerKey: "",
ServerCert: "",
AllowOrigin: "",
TrustedProxies: conf.IPNetworks{},
ReadTimeout: conf.StringDuration(10 * time.Second),
LocalUDPAddress: "127.0.0.1:8887",
LocalTCPAddress: "127.0.0.1:8887",
IPsFromInterfaces: true,
IPsFromInterfacesList: []string{},
AdditionalHosts: []string{},
ICEServers: []conf.WebRTCICEServer{},
HandshakeTimeout: conf.StringDuration(10 * time.Second),
TrackGatherTimeout: conf.StringDuration(2 * time.Second),
ExternalCmdPool: nil,
PathManager: pm,
Parent: test.NilLogger,
}
err = s.Initialize()
require.NoError(t, err)
defer s.Close()
tr := &http.Transport{}
defer tr.CloseIdleConnections()
hc := &http.Client{Transport: tr}
pc, err := pwebrtc.NewPeerConnection(pwebrtc.Configuration{})
require.NoError(t, err)
defer pc.Close() //nolint:errcheck
_, err = pc.AddTransceiverFromKind(pwebrtc.RTPCodecTypeVideo)
require.NoError(t, err)
offer, err := pc.CreateOffer(nil)
require.NoError(t, err)
req, err := http.NewRequest(http.MethodPost,
"http://localhost:8886/teststream/whep", bytes.NewReader([]byte(offer.SDP)))
require.NoError(t, err)
req.Header.Set("Content-Type", "application/sdp")
req.Header.Set("Authorization", "Bearer testing")
res, err := hc.Do(req)
require.NoError(t, err)
defer res.Body.Close()
require.Equal(t, http.StatusCreated, res.StatusCode)
}
func TestServerReadAuthorizationUserPass(t *testing.T) {
desc := &description.Session{Medias: []*description.Media{test.MediaH264}}
str, err := stream.New(
512,
1460,
desc,
true,
test.NilLogger,
)
require.NoError(t, err)
path := &dummyPath{stream: str}
pm := &dummyPathManager{
findPathConf: func(req defs.PathFindPathConfReq) (*conf.Path, error) {
require.Equal(t, "myuser", req.AccessRequest.User)
require.Equal(t, "mypass", req.AccessRequest.Pass)
return &conf.Path{}, nil
},
addReader: func(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) {
require.Equal(t, "myuser", req.AccessRequest.User)
require.Equal(t, "mypass", req.AccessRequest.Pass)
return path, str, nil
},
}
s := &Server{
Address: "127.0.0.1:8886",
Encryption: false,
ServerKey: "",
ServerCert: "",
AllowOrigin: "",
TrustedProxies: conf.IPNetworks{},
ReadTimeout: conf.StringDuration(10 * time.Second),
LocalUDPAddress: "127.0.0.1:8887",
LocalTCPAddress: "127.0.0.1:8887",
IPsFromInterfaces: true,
IPsFromInterfacesList: []string{},
AdditionalHosts: []string{},
ICEServers: []conf.WebRTCICEServer{},
HandshakeTimeout: conf.StringDuration(10 * time.Second),
TrackGatherTimeout: conf.StringDuration(2 * time.Second),
ExternalCmdPool: nil,
PathManager: pm,
Parent: test.NilLogger,
}
err = s.Initialize()
require.NoError(t, err)
defer s.Close()
tr := &http.Transport{}
defer tr.CloseIdleConnections()
hc := &http.Client{Transport: tr}
pc, err := pwebrtc.NewPeerConnection(pwebrtc.Configuration{})
require.NoError(t, err)
defer pc.Close() //nolint:errcheck
_, err = pc.AddTransceiverFromKind(pwebrtc.RTPCodecTypeVideo)
require.NoError(t, err)
offer, err := pc.CreateOffer(nil)
require.NoError(t, err)
req, err := http.NewRequest(http.MethodPost,
"http://localhost:8886/teststream/whep", bytes.NewReader([]byte(offer.SDP)))
require.NoError(t, err)
req.Header.Set("Content-Type", "application/sdp")
req.Header.Set("Authorization", "Bearer myuser:mypass")
res, err := hc.Do(req)
require.NoError(t, err)
defer res.Body.Close()
require.Equal(t, http.StatusCreated, res.StatusCode)
}
func TestServerReadNotFound(t *testing.T) {
pm := &dummyPathManager{
findPathConf: func(req defs.PathFindPathConfReq) (*conf.Path, error) {
require.Equal(t, "myuser", req.AccessRequest.User)
require.Equal(t, "mypass", req.AccessRequest.Pass)
findPathConf: func(_ defs.PathFindPathConfReq) (*conf.Path, error) {
return &conf.Path{}, nil
},
addReader: func(_ defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) {

View File

@@ -130,24 +130,14 @@ func (s *session) runPublish() (int, error) {
Author: s,
AccessRequest: defs.PathAccessRequest{
Name: s.req.pathName,
Query: s.req.query,
Publish: true,
IP: net.ParseIP(ip),
User: s.req.user,
Pass: s.req.pass,
Proto: auth.ProtocolWebRTC,
ID: &s.uuid,
HTTPRequest: s.req.httpRequest,
},
})
if err != nil {
var terr auth.Error
if errors.As(err, &terr) {
// wait some seconds to mitigate brute force attacks
<-time.After(auth.PauseAfterError)
return http.StatusUnauthorized, err
}
return http.StatusBadRequest, err
}
@@ -251,22 +241,13 @@ func (s *session) runRead() (int, error) {
Author: s,
AccessRequest: defs.PathAccessRequest{
Name: s.req.pathName,
Query: s.req.query,
IP: net.ParseIP(ip),
User: s.req.user,
Pass: s.req.pass,
Proto: auth.ProtocolWebRTC,
ID: &s.uuid,
HTTPRequest: s.req.httpRequest,
},
})
if err != nil {
var terr1 auth.Error
if errors.As(err, &terr1) {
// wait some seconds to mitigate brute force attacks
<-time.After(auth.PauseAfterError)
return http.StatusUnauthorized, err
}
var terr2 defs.PathNoOnePublishingError
if errors.As(err, &terr2) {
return http.StatusNotFound, err
@@ -338,7 +319,7 @@ func (s *session) runRead() (int, error) {
Conf: path.SafeConf(),
ExternalCmdEnv: path.ExternalCmdEnv(),
Reader: s.APIReaderDescribe(),
Query: s.req.query,
Query: s.req.httpRequest.URL.RawQuery,
})
defer onUnreadHook()
@@ -451,7 +432,7 @@ func (s *session) apiItem() *defs.APIWebRTCSession {
return defs.APIWebRTCSessionStateRead
}(),
Path: s.req.pathName,
Query: s.req.query,
Query: s.req.httpRequest.URL.RawQuery,
BytesReceived: bytesReceived,
BytesSent: bytesSent,
}

View File

@@ -4,17 +4,17 @@ import "github.com/bluenviron/mediamtx/internal/auth"
// AuthManager is a test auth manager.
type AuthManager struct {
Func func(req *auth.Request) error
fnc func(req *auth.Request) error
}
// Authenticate replicates auth.Manager.Replicate
func (m *AuthManager) Authenticate(req *auth.Request) error {
return m.Func(req)
return m.fnc(req)
}
// NilAuthManager is an auth manager that accepts everything.
var NilAuthManager = &AuthManager{
Func: func(_ *auth.Request) error {
fnc: func(_ *auth.Request) error {
return nil
},
}