mirror of
https://github.com/aler9/rtsp-simple-server
synced 2025-11-01 11:32:42 +08:00
This commit is contained in:
@@ -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
|
ffmpeg -re -stream_loop -1 -i file.ts -c copy -f rtsp rtsp://localhost:8554/mystream?jwt=MY_JWT
|
||||||
|
|||||||
@@ -284,17 +284,13 @@ func (a *API) middlewareOrigin(ctx *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *API) middlewareAuth(ctx *gin.Context) {
|
func (a *API) middlewareAuth(ctx *gin.Context) {
|
||||||
user, pass, hasCredentials := ctx.Request.BasicAuth()
|
|
||||||
|
|
||||||
err := a.AuthManager.Authenticate(&auth.Request{
|
err := a.AuthManager.Authenticate(&auth.Request{
|
||||||
User: user,
|
|
||||||
Pass: pass,
|
|
||||||
Query: ctx.Request.URL.RawQuery,
|
|
||||||
IP: net.ParseIP(ctx.ClientIP()),
|
IP: net.ParseIP(ctx.ClientIP()),
|
||||||
Action: conf.AuthActionAPI,
|
Action: conf.AuthActionAPI,
|
||||||
|
HTTPRequest: ctx.Request,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !hasCredentials {
|
if err.(*auth.Error).AskCredentials { //nolint:errorlint
|
||||||
ctx.Header("WWW-Authenticate", `Basic realm="mediamtx"`)
|
ctx.Header("WWW-Authenticate", `Basic realm="mediamtx"`)
|
||||||
ctx.AbortWithStatus(http.StatusUnauthorized)
|
ctx.AbortWithStatus(http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/bluenviron/mediamtx/internal/auth"
|
|
||||||
"github.com/bluenviron/mediamtx/internal/conf"
|
"github.com/bluenviron/mediamtx/internal/conf"
|
||||||
"github.com/bluenviron/mediamtx/internal/logger"
|
"github.com/bluenviron/mediamtx/internal/logger"
|
||||||
"github.com/bluenviron/mediamtx/internal/test"
|
"github.com/bluenviron/mediamtx/internal/test"
|
||||||
@@ -111,40 +110,6 @@ func TestPreflightRequest(t *testing.T) {
|
|||||||
require.Equal(t, byts, []byte{})
|
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) {
|
func TestConfigGlobalGet(t *testing.T) {
|
||||||
cnf := tempConf(t, "api: yes\n")
|
cnf := tempConf(t, "api: yes\n")
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,17 @@ const (
|
|||||||
jwtRefreshPeriod = 60 * 60 * time.Second
|
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.
|
// Protocol is a protocol.
|
||||||
type Protocol string
|
type Protocol string
|
||||||
|
|
||||||
@@ -55,17 +66,23 @@ type Request struct {
|
|||||||
Protocol Protocol
|
Protocol Protocol
|
||||||
ID *uuid.UUID
|
ID *uuid.UUID
|
||||||
Query string
|
Query string
|
||||||
|
|
||||||
|
// RTSP only
|
||||||
RTSPRequest *base.Request
|
RTSPRequest *base.Request
|
||||||
RTSPNonce string
|
RTSPNonce string
|
||||||
|
|
||||||
|
// HTTP only
|
||||||
|
HTTPRequest *http.Request
|
||||||
}
|
}
|
||||||
|
|
||||||
// Error is a authentication error.
|
// Error is a authentication error.
|
||||||
type Error struct {
|
type Error struct {
|
||||||
Message string
|
Message string
|
||||||
|
AskCredentials bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Error implements the error interface.
|
// Error implements the error interface.
|
||||||
func (e Error) Error() string {
|
func (e *Error) Error() string {
|
||||||
return "authentication failed: " + e.Message
|
return "authentication failed: " + e.Message
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,15 +171,6 @@ func (m *Manager) ReloadInternalUsers(u []conf.AuthInternalUser) {
|
|||||||
|
|
||||||
// Authenticate authenticates a request.
|
// Authenticate authenticates a request.
|
||||||
func (m *Manager) Authenticate(req *Request) error {
|
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
|
var rtspAuthHeader headers.Authorization
|
||||||
|
|
||||||
if req.RTSPRequest != nil {
|
if req.RTSPRequest != nil {
|
||||||
@@ -175,18 +183,42 @@ func (m *Manager) authenticateInner(req *Request) error {
|
|||||||
req.User = rtspAuthHeader.Username
|
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 {
|
switch m.Method {
|
||||||
case conf.AuthMethodInternal:
|
case conf.AuthMethodInternal:
|
||||||
return m.authenticateInternal(req, &rtspAuthHeader)
|
err = m.authenticateInternal(req, &rtspAuthHeader)
|
||||||
|
|
||||||
case conf.AuthMethodHTTP:
|
case conf.AuthMethodHTTP:
|
||||||
return m.authenticateHTTP(req)
|
err = m.authenticateHTTP(req)
|
||||||
|
|
||||||
default:
|
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 {
|
func (m *Manager) authenticateInternal(req *Request, rtspAuthHeader *headers.Authorization) error {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -186,6 +187,37 @@ func TestAuthInternalRTSPDigest(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
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) {
|
func TestAuthHTTP(t *testing.T) {
|
||||||
for _, outcome := range []string{"ok", "fail"} {
|
for _, outcome := range []string{"ok", "fail"} {
|
||||||
t.Run(outcome, func(t *testing.T) {
|
t.Run(outcome, func(t *testing.T) {
|
||||||
@@ -292,6 +324,8 @@ func TestAuthJWT(t *testing.T) {
|
|||||||
// taken from
|
// taken from
|
||||||
// https://github.com/MicahParks/jwkset/blob/master/examples/http_server/main.go
|
// 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)
|
key, err := rsa.GenerateKey(rand.Reader, 1024)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@@ -356,14 +390,27 @@ func TestAuthJWT(t *testing.T) {
|
|||||||
JWTClaimKey: "my_permission_key",
|
JWTClaimKey: "my_permission_key",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ca == "query" {
|
||||||
err = m.Authenticate(&Request{
|
err = m.Authenticate(&Request{
|
||||||
User: "",
|
|
||||||
Pass: "",
|
|
||||||
IP: net.ParseIP("127.0.0.1"),
|
IP: net.ParseIP("127.0.0.1"),
|
||||||
Action: conf.AuthActionPublish,
|
Action: conf.AuthActionPublish,
|
||||||
Path: "mypath",
|
Path: "mypath",
|
||||||
Protocol: ProtocolRTSP,
|
Protocol: ProtocolRTSP,
|
||||||
Query: "param=value&jwt=" + ss,
|
Query: "param=value&jwt=" + ss,
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
} 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)
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package defs
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
"github.com/bluenviron/gortsplib/v4/pkg/base"
|
"github.com/bluenviron/gortsplib/v4/pkg/base"
|
||||||
"github.com/bluenviron/gortsplib/v4/pkg/description"
|
"github.com/bluenviron/gortsplib/v4/pkg/description"
|
||||||
@@ -35,7 +36,7 @@ type Path interface {
|
|||||||
RemoveReader(req PathRemoveReaderReq)
|
RemoveReader(req PathRemoveReaderReq)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PathAccessRequest is an access request.
|
// PathAccessRequest is a path access request.
|
||||||
type PathAccessRequest struct {
|
type PathAccessRequest struct {
|
||||||
Name string
|
Name string
|
||||||
Query string
|
Query string
|
||||||
@@ -43,13 +44,18 @@ type PathAccessRequest struct {
|
|||||||
SkipAuth bool
|
SkipAuth bool
|
||||||
|
|
||||||
// only if skipAuth = false
|
// only if skipAuth = false
|
||||||
IP net.IP
|
|
||||||
User string
|
User string
|
||||||
Pass string
|
Pass string
|
||||||
|
IP net.IP
|
||||||
Proto auth.Protocol
|
Proto auth.Protocol
|
||||||
ID *uuid.UUID
|
ID *uuid.UUID
|
||||||
|
|
||||||
|
// RTSP only
|
||||||
RTSPRequest *base.Request
|
RTSPRequest *base.Request
|
||||||
RTSPNonce string
|
RTSPNonce string
|
||||||
|
|
||||||
|
// HTTP only
|
||||||
|
HTTPRequest *http.Request
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToAuthRequest converts a path access request into an authentication request.
|
// ToAuthRequest converts a path access request into an authentication request.
|
||||||
@@ -70,6 +76,7 @@ func (r *PathAccessRequest) ToAuthRequest() *auth.Request {
|
|||||||
Query: r.Query,
|
Query: r.Query,
|
||||||
RTSPRequest: r.RTSPRequest,
|
RTSPRequest: r.RTSPRequest,
|
||||||
RTSPNonce: r.RTSPNonce,
|
RTSPNonce: r.RTSPNonce,
|
||||||
|
HTTPRequest: r.HTTPRequest,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -120,17 +120,13 @@ func (m *Metrics) onRequest(ctx *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
user, pass, hasCredentials := ctx.Request.BasicAuth()
|
|
||||||
|
|
||||||
err := m.AuthManager.Authenticate(&auth.Request{
|
err := m.AuthManager.Authenticate(&auth.Request{
|
||||||
User: user,
|
|
||||||
Pass: pass,
|
|
||||||
Query: ctx.Request.URL.RawQuery,
|
|
||||||
IP: net.ParseIP(ctx.ClientIP()),
|
IP: net.ParseIP(ctx.ClientIP()),
|
||||||
Action: conf.AuthActionMetrics,
|
Action: conf.AuthActionMetrics,
|
||||||
|
HTTPRequest: ctx.Request,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !hasCredentials {
|
if err.(*auth.Error).AskCredentials { //nolint:errorlint
|
||||||
ctx.Header("WWW-Authenticate", `Basic realm="mediamtx"`)
|
ctx.Header("WWW-Authenticate", `Basic realm="mediamtx"`)
|
||||||
ctx.AbortWithStatus(http.StatusUnauthorized)
|
ctx.AbortWithStatus(http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import (
|
|||||||
"github.com/bluenviron/mediacommon/pkg/codecs/mpeg4audio"
|
"github.com/bluenviron/mediacommon/pkg/codecs/mpeg4audio"
|
||||||
"github.com/bluenviron/mediacommon/pkg/formats/fmp4"
|
"github.com/bluenviron/mediacommon/pkg/formats/fmp4"
|
||||||
"github.com/bluenviron/mediacommon/pkg/formats/fmp4/seekablebuffer"
|
"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/conf"
|
||||||
"github.com/bluenviron/mediamtx/internal/test"
|
"github.com/bluenviron/mediamtx/internal/test"
|
||||||
"github.com/stretchr/testify/require"
|
"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"),
|
RecordPath: filepath.Join(dir, "%path/%Y-%m-%d_%H-%M-%S-%f"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
AuthManager: &test.AuthManager{
|
AuthManager: test.NilAuthManager,
|
||||||
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
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Parent: test.NilLogger,
|
Parent: test.NilLogger,
|
||||||
}
|
}
|
||||||
err = s.Initialize()
|
err = s.Initialize()
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/bluenviron/mediamtx/internal/auth"
|
|
||||||
"github.com/bluenviron/mediamtx/internal/conf"
|
"github.com/bluenviron/mediamtx/internal/conf"
|
||||||
"github.com/bluenviron/mediamtx/internal/test"
|
"github.com/bluenviron/mediamtx/internal/test"
|
||||||
"github.com/stretchr/testify/require"
|
"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"),
|
RecordPath: filepath.Join(dir, "%path/%Y-%m-%d_%H-%M-%S-%f"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
AuthManager: &test.AuthManager{
|
AuthManager: test.NilAuthManager,
|
||||||
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
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Parent: test.NilLogger,
|
Parent: test.NilLogger,
|
||||||
}
|
}
|
||||||
err = s.Initialize()
|
err = s.Initialize()
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
package playback
|
package playback
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -119,27 +118,21 @@ func (s *Server) middlewareOrigin(ctx *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) doAuth(ctx *gin.Context, pathName string) bool {
|
func (s *Server) doAuth(ctx *gin.Context, pathName string) bool {
|
||||||
user, pass, hasCredentials := ctx.Request.BasicAuth()
|
|
||||||
|
|
||||||
err := s.AuthManager.Authenticate(&auth.Request{
|
err := s.AuthManager.Authenticate(&auth.Request{
|
||||||
User: user,
|
|
||||||
Pass: pass,
|
|
||||||
Query: ctx.Request.URL.RawQuery,
|
|
||||||
IP: net.ParseIP(ctx.ClientIP()),
|
IP: net.ParseIP(ctx.ClientIP()),
|
||||||
Action: conf.AuthActionPlayback,
|
Action: conf.AuthActionPlayback,
|
||||||
Path: pathName,
|
Path: pathName,
|
||||||
|
HTTPRequest: ctx.Request,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !hasCredentials {
|
if err.(*auth.Error).AskCredentials { //nolint:errorlint
|
||||||
ctx.Header("WWW-Authenticate", `Basic realm="mediamtx"`)
|
ctx.Header("WWW-Authenticate", `Basic realm="mediamtx"`)
|
||||||
ctx.Writer.WriteHeader(http.StatusUnauthorized)
|
ctx.Writer.WriteHeader(http.StatusUnauthorized)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
var terr auth.Error
|
s.Log(logger.Info, "connection %v failed to authenticate: %v",
|
||||||
errors.As(err, &terr)
|
httpp.RemoteAddr(ctx), err.(*auth.Error).Message) //nolint:errorlint
|
||||||
|
|
||||||
s.Log(logger.Info, "connection %v failed to authenticate: %v", httpp.RemoteAddr(ctx), terr.Message)
|
|
||||||
|
|
||||||
// wait some seconds to mitigate brute force attacks
|
// wait some seconds to mitigate brute force attacks
|
||||||
<-time.After(auth.PauseAfterError)
|
<-time.After(auth.PauseAfterError)
|
||||||
|
|||||||
@@ -92,17 +92,13 @@ func (pp *PPROF) onRequest(ctx *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
user, pass, hasCredentials := ctx.Request.BasicAuth()
|
|
||||||
|
|
||||||
err := pp.AuthManager.Authenticate(&auth.Request{
|
err := pp.AuthManager.Authenticate(&auth.Request{
|
||||||
User: user,
|
|
||||||
Pass: pass,
|
|
||||||
Query: ctx.Request.URL.RawQuery,
|
|
||||||
IP: net.ParseIP(ctx.ClientIP()),
|
IP: net.ParseIP(ctx.ClientIP()),
|
||||||
Action: conf.AuthActionMetrics,
|
Action: conf.AuthActionMetrics,
|
||||||
|
HTTPRequest: ctx.Request,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !hasCredentials {
|
if err.(*auth.Error).AskCredentials { //nolint:errorlint
|
||||||
ctx.Writer.Header().Set("WWW-Authenticate", `Basic realm="mediamtx"`)
|
ctx.Writer.Header().Set("WWW-Authenticate", `Basic realm="mediamtx"`)
|
||||||
ctx.Writer.WriteHeader(http.StatusUnauthorized)
|
ctx.Writer.WriteHeader(http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
|
||||||
gopath "path"
|
gopath "path"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -37,17 +36,6 @@ func mergePathAndQuery(path string, rawQuery string) string {
|
|||||||
return res
|
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 {
|
type httpServer struct {
|
||||||
address string
|
address string
|
||||||
encryption bool
|
encryption bool
|
||||||
@@ -157,28 +145,19 @@ func (s *httpServer) onRequest(ctx *gin.Context) {
|
|||||||
return
|
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{
|
pathConf, err := s.pathManager.FindPathConf(defs.PathFindPathConfReq{
|
||||||
AccessRequest: defs.PathAccessRequest{
|
AccessRequest: defs.PathAccessRequest{
|
||||||
Name: dir,
|
Name: dir,
|
||||||
Query: q,
|
|
||||||
Publish: false,
|
Publish: false,
|
||||||
IP: net.ParseIP(ctx.ClientIP()),
|
IP: net.ParseIP(ctx.ClientIP()),
|
||||||
User: user,
|
|
||||||
Pass: pass,
|
|
||||||
Proto: auth.ProtocolHLS,
|
Proto: auth.ProtocolHLS,
|
||||||
|
HTTPRequest: ctx.Request,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
var terr auth.Error
|
var terr *auth.Error
|
||||||
if errors.As(err, &terr) {
|
if errors.As(err, &terr) {
|
||||||
if !hasCredentials {
|
if terr.AskCredentials {
|
||||||
ctx.Header("WWW-Authenticate", `Basic realm="mediamtx"`)
|
ctx.Header("WWW-Authenticate", `Basic realm="mediamtx"`)
|
||||||
ctx.Writer.WriteHeader(http.StatusUnauthorized)
|
ctx.Writer.WriteHeader(http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -106,8 +106,6 @@ func TestServerNotFound(t *testing.T) {
|
|||||||
pm := &dummyPathManager{
|
pm := &dummyPathManager{
|
||||||
findPathConf: func(req defs.PathFindPathConfReq) (*conf.Path, error) {
|
findPathConf: func(req defs.PathFindPathConfReq) (*conf.Path, error) {
|
||||||
require.Equal(t, "nonexisting", req.AccessRequest.Name)
|
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
|
return &conf.Path{}, nil
|
||||||
},
|
},
|
||||||
addReader: func(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) {
|
addReader: func(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) {
|
||||||
@@ -181,8 +179,6 @@ func TestServerRead(t *testing.T) {
|
|||||||
pm := &dummyPathManager{
|
pm := &dummyPathManager{
|
||||||
findPathConf: func(req defs.PathFindPathConfReq) (*conf.Path, error) {
|
findPathConf: func(req defs.PathFindPathConfReq) (*conf.Path, error) {
|
||||||
require.Equal(t, "mystream", req.AccessRequest.Name)
|
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
|
return &conf.Path{}, nil
|
||||||
},
|
},
|
||||||
addReader: func(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) {
|
addReader: func(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) {
|
||||||
@@ -277,8 +273,6 @@ func TestServerRead(t *testing.T) {
|
|||||||
pm := &dummyPathManager{
|
pm := &dummyPathManager{
|
||||||
findPathConf: func(req defs.PathFindPathConfReq) (*conf.Path, error) {
|
findPathConf: func(req defs.PathFindPathConfReq) (*conf.Path, error) {
|
||||||
require.Equal(t, "mystream", req.AccessRequest.Name)
|
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
|
return &conf.Path{}, nil
|
||||||
},
|
},
|
||||||
addReader: func(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) {
|
addReader: func(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) {
|
||||||
@@ -372,8 +366,7 @@ func TestServerReadAuthorizationHeader(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
pm := &dummyPathManager{
|
pm := &dummyPathManager{
|
||||||
findPathConf: func(req defs.PathFindPathConfReq) (*conf.Path, error) {
|
findPathConf: func(_ defs.PathFindPathConfReq) (*conf.Path, error) {
|
||||||
require.Equal(t, "jwt=testing", req.AccessRequest.Query)
|
|
||||||
return &conf.Path{}, nil
|
return &conf.Path{}, nil
|
||||||
},
|
},
|
||||||
addReader: func(_ defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) {
|
addReader: func(_ defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) {
|
||||||
|
|||||||
@@ -168,7 +168,7 @@ func (c *conn) runRead(conn *rtmp.Conn, u *url.URL) error {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
var terr auth.Error
|
var terr *auth.Error
|
||||||
if errors.As(err, &terr) {
|
if errors.As(err, &terr) {
|
||||||
// wait some seconds to mitigate brute force attacks
|
// wait some seconds to mitigate brute force attacks
|
||||||
<-time.After(auth.PauseAfterError)
|
<-time.After(auth.PauseAfterError)
|
||||||
@@ -235,7 +235,7 @@ func (c *conn) runPublish(conn *rtmp.Conn, u *url.URL) error {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
var terr auth.Error
|
var terr *auth.Error
|
||||||
if errors.As(err, &terr) {
|
if errors.As(err, &terr) {
|
||||||
// wait some seconds to mitigate brute force attacks
|
// wait some seconds to mitigate brute force attacks
|
||||||
<-time.After(auth.PauseAfterError)
|
<-time.After(auth.PauseAfterError)
|
||||||
|
|||||||
@@ -68,14 +68,14 @@ type dummyPathManager struct {
|
|||||||
|
|
||||||
func (pm *dummyPathManager) AddPublisher(req defs.PathAddPublisherReq) (defs.Path, error) {
|
func (pm *dummyPathManager) AddPublisher(req defs.PathAddPublisherReq) (defs.Path, error) {
|
||||||
if req.AccessRequest.User != "myuser" || req.AccessRequest.Pass != "mypass" {
|
if req.AccessRequest.User != "myuser" || req.AccessRequest.Pass != "mypass" {
|
||||||
return nil, auth.Error{}
|
return nil, &auth.Error{}
|
||||||
}
|
}
|
||||||
return pm.path, nil
|
return pm.path, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pm *dummyPathManager) AddReader(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) {
|
func (pm *dummyPathManager) AddReader(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) {
|
||||||
if req.AccessRequest.User != "myuser" || req.AccessRequest.Pass != "mypass" {
|
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
|
return pm.path, pm.path.stream, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ func (c *conn) onDescribe(ctx *gortsplib.ServerHandlerOnDescribeCtx,
|
|||||||
})
|
})
|
||||||
|
|
||||||
if res.Err != nil {
|
if res.Err != nil {
|
||||||
var terr auth.Error
|
var terr *auth.Error
|
||||||
if errors.As(res.Err, &terr) {
|
if errors.As(res.Err, &terr) {
|
||||||
res, err := c.handleAuthError(terr)
|
res, err := c.handleAuthError(terr)
|
||||||
return res, nil, err
|
return res, nil, err
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ func (s *session) onAnnounce(c *conn, ctx *gortsplib.ServerHandlerOnAnnounceCtx)
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
var terr auth.Error
|
var terr *auth.Error
|
||||||
if errors.As(err, &terr) {
|
if errors.As(err, &terr) {
|
||||||
return c.handleAuthError(terr)
|
return c.handleAuthError(terr)
|
||||||
}
|
}
|
||||||
@@ -195,7 +195,7 @@ func (s *session) onSetup(c *conn, ctx *gortsplib.ServerHandlerOnSetupCtx,
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
var terr auth.Error
|
var terr *auth.Error
|
||||||
if errors.As(err, &terr) {
|
if errors.As(err, &terr) {
|
||||||
res, err2 := c.handleAuthError(terr)
|
res, err2 := c.handleAuthError(terr)
|
||||||
return res, nil, err2
|
return res, nil, err2
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ func (c *conn) runPublish(streamID *streamID) error {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
var terr auth.Error
|
var terr *auth.Error
|
||||||
if errors.As(err, &terr) {
|
if errors.As(err, &terr) {
|
||||||
// wait some seconds to mitigate brute force attacks
|
// wait some seconds to mitigate brute force attacks
|
||||||
<-time.After(auth.PauseAfterError)
|
<-time.After(auth.PauseAfterError)
|
||||||
@@ -250,7 +250,7 @@ func (c *conn) runRead(streamID *streamID) error {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
var terr auth.Error
|
var terr *auth.Error
|
||||||
if errors.As(err, &terr) {
|
if errors.As(err, &terr) {
|
||||||
// wait some seconds to mitigate brute force attacks
|
// wait some seconds to mitigate brute force attacks
|
||||||
<-time.After(auth.PauseAfterError)
|
<-time.After(auth.PauseAfterError)
|
||||||
|
|||||||
@@ -66,14 +66,14 @@ type dummyPathManager struct {
|
|||||||
|
|
||||||
func (pm *dummyPathManager) AddPublisher(req defs.PathAddPublisherReq) (defs.Path, error) {
|
func (pm *dummyPathManager) AddPublisher(req defs.PathAddPublisherReq) (defs.Path, error) {
|
||||||
if req.AccessRequest.User != "myuser" || req.AccessRequest.Pass != "mypass" {
|
if req.AccessRequest.User != "myuser" || req.AccessRequest.Pass != "mypass" {
|
||||||
return nil, auth.Error{}
|
return nil, &auth.Error{}
|
||||||
}
|
}
|
||||||
return pm.path, nil
|
return pm.path, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pm *dummyPathManager) AddReader(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) {
|
func (pm *dummyPathManager) AddReader(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) {
|
||||||
if req.AccessRequest.User != "myuser" || req.AccessRequest.Pass != "mypass" {
|
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
|
return pm.path, pm.path.stream, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -60,17 +59,6 @@ func sessionLocation(publish bool, path string, secret uuid.UUID) string {
|
|||||||
return ret
|
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 {
|
type httpServer struct {
|
||||||
address string
|
address string
|
||||||
encryption bool
|
encryption bool
|
||||||
@@ -120,35 +108,19 @@ func (s *httpServer) close() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *httpServer) checkAuthOutsideSession(ctx *gin.Context, pathName string, publish bool) bool {
|
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{
|
_, err := s.pathManager.FindPathConf(defs.PathFindPathConfReq{
|
||||||
AccessRequest: defs.PathAccessRequest{
|
AccessRequest: defs.PathAccessRequest{
|
||||||
Name: pathName,
|
Name: pathName,
|
||||||
Query: q,
|
|
||||||
Publish: publish,
|
Publish: publish,
|
||||||
IP: net.ParseIP(ctx.ClientIP()),
|
IP: net.ParseIP(ctx.ClientIP()),
|
||||||
User: user,
|
|
||||||
Pass: pass,
|
|
||||||
Proto: auth.ProtocolWebRTC,
|
Proto: auth.ProtocolWebRTC,
|
||||||
|
HTTPRequest: ctx.Request,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
var terr auth.Error
|
var terr *auth.Error
|
||||||
if errors.As(err, &terr) {
|
if errors.As(err, &terr) {
|
||||||
if !hasCredentials {
|
if terr.AskCredentials {
|
||||||
ctx.Header("WWW-Authenticate", `Basic realm="mediamtx"`)
|
ctx.Header("WWW-Authenticate", `Basic realm="mediamtx"`)
|
||||||
ctx.Writer.WriteHeader(http.StatusUnauthorized)
|
ctx.Writer.WriteHeader(http.StatusUnauthorized)
|
||||||
return false
|
return false
|
||||||
@@ -200,30 +172,31 @@ func (s *httpServer) onWHIPPost(ctx *gin.Context, pathName string, publish bool)
|
|||||||
return
|
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{
|
res := s.parent.newSession(webRTCNewSessionReq{
|
||||||
pathName: pathName,
|
pathName: pathName,
|
||||||
remoteAddr: httpp.RemoteAddr(ctx),
|
remoteAddr: httpp.RemoteAddr(ctx),
|
||||||
query: q,
|
|
||||||
user: user,
|
|
||||||
pass: pass,
|
|
||||||
offer: offer,
|
offer: offer,
|
||||||
publish: publish,
|
publish: publish,
|
||||||
|
httpRequest: ctx.Request,
|
||||||
})
|
})
|
||||||
if res.err != nil {
|
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)
|
writeError(ctx, res.errStatusCode, res.err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -135,11 +135,9 @@ type webRTCNewSessionRes struct {
|
|||||||
type webRTCNewSessionReq struct {
|
type webRTCNewSessionReq struct {
|
||||||
pathName string
|
pathName string
|
||||||
remoteAddr string
|
remoteAddr string
|
||||||
query string
|
|
||||||
user string
|
|
||||||
pass string
|
|
||||||
offer []byte
|
offer []byte
|
||||||
publish bool
|
publish bool
|
||||||
|
httpRequest *http.Request
|
||||||
res chan webRTCNewSessionRes
|
res chan webRTCNewSessionRes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -96,9 +96,7 @@ func (pm *dummyPathManager) AddReader(req defs.PathAddReaderReq) (defs.Path, *st
|
|||||||
|
|
||||||
func initializeTestServer(t *testing.T) *Server {
|
func initializeTestServer(t *testing.T) *Server {
|
||||||
pm := &dummyPathManager{
|
pm := &dummyPathManager{
|
||||||
findPathConf: func(req defs.PathFindPathConfReq) (*conf.Path, error) {
|
findPathConf: func(_ defs.PathFindPathConfReq) (*conf.Path, error) {
|
||||||
require.Equal(t, "myuser", req.AccessRequest.User)
|
|
||||||
require.Equal(t, "mypass", req.AccessRequest.Pass)
|
|
||||||
return &conf.Path{}, nil
|
return &conf.Path{}, nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -182,9 +180,7 @@ func TestPreflightRequest(t *testing.T) {
|
|||||||
|
|
||||||
func TestServerOptionsICEServer(t *testing.T) {
|
func TestServerOptionsICEServer(t *testing.T) {
|
||||||
pathManager := &dummyPathManager{
|
pathManager := &dummyPathManager{
|
||||||
findPathConf: func(req defs.PathFindPathConfReq) (*conf.Path, error) {
|
findPathConf: func(_ defs.PathFindPathConfReq) (*conf.Path, error) {
|
||||||
require.Equal(t, "myuser", req.AccessRequest.User)
|
|
||||||
require.Equal(t, "mypass", req.AccessRequest.Pass)
|
|
||||||
return &conf.Path{}, nil
|
return &conf.Path{}, nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -249,14 +245,10 @@ func TestServerPublish(t *testing.T) {
|
|||||||
pathManager := &dummyPathManager{
|
pathManager := &dummyPathManager{
|
||||||
findPathConf: func(req defs.PathFindPathConfReq) (*conf.Path, error) {
|
findPathConf: func(req defs.PathFindPathConfReq) (*conf.Path, error) {
|
||||||
require.Equal(t, "teststream", req.AccessRequest.Name)
|
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
|
return &conf.Path{}, nil
|
||||||
},
|
},
|
||||||
addPublisher: func(req defs.PathAddPublisherReq) (defs.Path, error) {
|
addPublisher: func(req defs.PathAddPublisherReq) (defs.Path, error) {
|
||||||
require.Equal(t, "teststream", req.AccessRequest.Name)
|
require.Equal(t, "teststream", req.AccessRequest.Name)
|
||||||
require.Equal(t, "myuser", req.AccessRequest.User)
|
|
||||||
require.Equal(t, "mypass", req.AccessRequest.Pass)
|
|
||||||
return path, nil
|
return path, nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -534,14 +526,10 @@ func TestServerRead(t *testing.T) {
|
|||||||
pathManager := &dummyPathManager{
|
pathManager := &dummyPathManager{
|
||||||
findPathConf: func(req defs.PathFindPathConfReq) (*conf.Path, error) {
|
findPathConf: func(req defs.PathFindPathConfReq) (*conf.Path, error) {
|
||||||
require.Equal(t, "teststream", req.AccessRequest.Name)
|
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
|
return &conf.Path{}, nil
|
||||||
},
|
},
|
||||||
addReader: func(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) {
|
addReader: func(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) {
|
||||||
require.Equal(t, "teststream", req.AccessRequest.Name)
|
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
|
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) {
|
func TestServerReadNotFound(t *testing.T) {
|
||||||
pm := &dummyPathManager{
|
pm := &dummyPathManager{
|
||||||
findPathConf: func(req defs.PathFindPathConfReq) (*conf.Path, error) {
|
findPathConf: func(_ defs.PathFindPathConfReq) (*conf.Path, error) {
|
||||||
require.Equal(t, "myuser", req.AccessRequest.User)
|
|
||||||
require.Equal(t, "mypass", req.AccessRequest.Pass)
|
|
||||||
return &conf.Path{}, nil
|
return &conf.Path{}, nil
|
||||||
},
|
},
|
||||||
addReader: func(_ defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) {
|
addReader: func(_ defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) {
|
||||||
|
|||||||
@@ -130,24 +130,14 @@ func (s *session) runPublish() (int, error) {
|
|||||||
Author: s,
|
Author: s,
|
||||||
AccessRequest: defs.PathAccessRequest{
|
AccessRequest: defs.PathAccessRequest{
|
||||||
Name: s.req.pathName,
|
Name: s.req.pathName,
|
||||||
Query: s.req.query,
|
|
||||||
Publish: true,
|
Publish: true,
|
||||||
IP: net.ParseIP(ip),
|
IP: net.ParseIP(ip),
|
||||||
User: s.req.user,
|
|
||||||
Pass: s.req.pass,
|
|
||||||
Proto: auth.ProtocolWebRTC,
|
Proto: auth.ProtocolWebRTC,
|
||||||
ID: &s.uuid,
|
ID: &s.uuid,
|
||||||
|
HTTPRequest: s.req.httpRequest,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
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
|
return http.StatusBadRequest, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -251,22 +241,13 @@ func (s *session) runRead() (int, error) {
|
|||||||
Author: s,
|
Author: s,
|
||||||
AccessRequest: defs.PathAccessRequest{
|
AccessRequest: defs.PathAccessRequest{
|
||||||
Name: s.req.pathName,
|
Name: s.req.pathName,
|
||||||
Query: s.req.query,
|
|
||||||
IP: net.ParseIP(ip),
|
IP: net.ParseIP(ip),
|
||||||
User: s.req.user,
|
|
||||||
Pass: s.req.pass,
|
|
||||||
Proto: auth.ProtocolWebRTC,
|
Proto: auth.ProtocolWebRTC,
|
||||||
ID: &s.uuid,
|
ID: &s.uuid,
|
||||||
|
HTTPRequest: s.req.httpRequest,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
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
|
var terr2 defs.PathNoOnePublishingError
|
||||||
if errors.As(err, &terr2) {
|
if errors.As(err, &terr2) {
|
||||||
return http.StatusNotFound, err
|
return http.StatusNotFound, err
|
||||||
@@ -338,7 +319,7 @@ func (s *session) runRead() (int, error) {
|
|||||||
Conf: path.SafeConf(),
|
Conf: path.SafeConf(),
|
||||||
ExternalCmdEnv: path.ExternalCmdEnv(),
|
ExternalCmdEnv: path.ExternalCmdEnv(),
|
||||||
Reader: s.APIReaderDescribe(),
|
Reader: s.APIReaderDescribe(),
|
||||||
Query: s.req.query,
|
Query: s.req.httpRequest.URL.RawQuery,
|
||||||
})
|
})
|
||||||
defer onUnreadHook()
|
defer onUnreadHook()
|
||||||
|
|
||||||
@@ -451,7 +432,7 @@ func (s *session) apiItem() *defs.APIWebRTCSession {
|
|||||||
return defs.APIWebRTCSessionStateRead
|
return defs.APIWebRTCSessionStateRead
|
||||||
}(),
|
}(),
|
||||||
Path: s.req.pathName,
|
Path: s.req.pathName,
|
||||||
Query: s.req.query,
|
Query: s.req.httpRequest.URL.RawQuery,
|
||||||
BytesReceived: bytesReceived,
|
BytesReceived: bytesReceived,
|
||||||
BytesSent: bytesSent,
|
BytesSent: bytesSent,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,17 +4,17 @@ import "github.com/bluenviron/mediamtx/internal/auth"
|
|||||||
|
|
||||||
// AuthManager is a test auth manager.
|
// AuthManager is a test auth manager.
|
||||||
type AuthManager struct {
|
type AuthManager struct {
|
||||||
Func func(req *auth.Request) error
|
fnc func(req *auth.Request) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// Authenticate replicates auth.Manager.Replicate
|
// Authenticate replicates auth.Manager.Replicate
|
||||||
func (m *AuthManager) Authenticate(req *auth.Request) error {
|
func (m *AuthManager) Authenticate(req *auth.Request) error {
|
||||||
return m.Func(req)
|
return m.fnc(req)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NilAuthManager is an auth manager that accepts everything.
|
// NilAuthManager is an auth manager that accepts everything.
|
||||||
var NilAuthManager = &AuthManager{
|
var NilAuthManager = &AuthManager{
|
||||||
Func: func(_ *auth.Request) error {
|
fnc: func(_ *auth.Request) error {
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user