Browse Source
This is a new authentication system that covers all the features exposed by the server, including playback, API, metrics and PPROF, improves internal authentication by adding permissions, improves HTTP-based authentication by adding the ability to exclude certain actions from being authenticated, adds an additional method (JWT-based authentication).pull/3096/head
53 changed files with 1976 additions and 947 deletions
@ -0,0 +1,327 @@ |
|||||||
|
// Package auth contains the authentication system.
|
||||||
|
package auth |
||||||
|
|
||||||
|
import ( |
||||||
|
"bytes" |
||||||
|
"encoding/json" |
||||||
|
"fmt" |
||||||
|
"io" |
||||||
|
"net" |
||||||
|
"net/http" |
||||||
|
"net/url" |
||||||
|
"regexp" |
||||||
|
"strings" |
||||||
|
"sync" |
||||||
|
"time" |
||||||
|
|
||||||
|
"github.com/MicahParks/keyfunc/v3" |
||||||
|
"github.com/bluenviron/gortsplib/v4/pkg/auth" |
||||||
|
"github.com/bluenviron/gortsplib/v4/pkg/base" |
||||||
|
"github.com/bluenviron/gortsplib/v4/pkg/headers" |
||||||
|
"github.com/bluenviron/mediamtx/internal/conf" |
||||||
|
"github.com/golang-jwt/jwt/v5" |
||||||
|
"github.com/google/uuid" |
||||||
|
) |
||||||
|
|
||||||
|
const ( |
||||||
|
// PauseAfterError is the pause to apply after an authentication failure.
|
||||||
|
PauseAfterError = 2 * time.Second |
||||||
|
|
||||||
|
rtspAuthRealm = "IPCAM" |
||||||
|
jwtRefreshPeriod = 60 * 60 * time.Second |
||||||
|
) |
||||||
|
|
||||||
|
// Protocol is a protocol.
|
||||||
|
type Protocol string |
||||||
|
|
||||||
|
// protocols.
|
||||||
|
const ( |
||||||
|
ProtocolRTSP Protocol = "rtsp" |
||||||
|
ProtocolRTMP Protocol = "rtmp" |
||||||
|
ProtocolHLS Protocol = "hls" |
||||||
|
ProtocolWebRTC Protocol = "webrtc" |
||||||
|
ProtocolSRT Protocol = "srt" |
||||||
|
) |
||||||
|
|
||||||
|
// Request is an authentication request.
|
||||||
|
type Request struct { |
||||||
|
User string |
||||||
|
Pass string |
||||||
|
IP net.IP |
||||||
|
Action conf.AuthAction |
||||||
|
|
||||||
|
// only for ActionPublish, ActionRead, ActionPlayback
|
||||||
|
Path string |
||||||
|
Protocol Protocol |
||||||
|
ID *uuid.UUID |
||||||
|
Query string |
||||||
|
RTSPRequest *base.Request |
||||||
|
RTSPBaseURL *base.URL |
||||||
|
RTSPNonce string |
||||||
|
} |
||||||
|
|
||||||
|
// Error is a authentication error.
|
||||||
|
type Error struct { |
||||||
|
Message string |
||||||
|
} |
||||||
|
|
||||||
|
// Error implements the error interface.
|
||||||
|
func (e Error) Error() string { |
||||||
|
return "authentication failed: " + e.Message |
||||||
|
} |
||||||
|
|
||||||
|
func matchesPermission(perms []conf.AuthInternalUserPermission, req *Request) bool { |
||||||
|
for _, perm := range perms { |
||||||
|
if perm.Action == req.Action { |
||||||
|
if perm.Action == conf.AuthActionPublish || |
||||||
|
perm.Action == conf.AuthActionRead || |
||||||
|
perm.Action == conf.AuthActionPlayback { |
||||||
|
switch { |
||||||
|
case perm.Path == "": |
||||||
|
return true |
||||||
|
|
||||||
|
case strings.HasPrefix(perm.Path, "~"): |
||||||
|
regexp, err := regexp.Compile(perm.Path[1:]) |
||||||
|
if err == nil && regexp.MatchString(req.Path) { |
||||||
|
return true |
||||||
|
} |
||||||
|
|
||||||
|
case perm.Path == req.Path: |
||||||
|
return true |
||||||
|
} |
||||||
|
} else { |
||||||
|
return true |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
type customClaims struct { |
||||||
|
jwt.RegisteredClaims |
||||||
|
MediaMTXPermissions []conf.AuthInternalUserPermission `json:"mediamtx_permissions"` |
||||||
|
} |
||||||
|
|
||||||
|
// Manager is the authentication manager.
|
||||||
|
type Manager struct { |
||||||
|
Method conf.AuthMethod |
||||||
|
InternalUsers []conf.AuthInternalUser |
||||||
|
HTTPAddress string |
||||||
|
HTTPExclude []conf.AuthInternalUserPermission |
||||||
|
JWTJWKS string |
||||||
|
ReadTimeout time.Duration |
||||||
|
RTSPAuthMethods []headers.AuthMethod |
||||||
|
|
||||||
|
mutex sync.RWMutex |
||||||
|
jwtHTTPClient *http.Client |
||||||
|
jwtLastRefresh time.Time |
||||||
|
jwtKeyFunc keyfunc.Keyfunc |
||||||
|
} |
||||||
|
|
||||||
|
// ReloadInternalUsers reloads InternalUsers.
|
||||||
|
func (m *Manager) ReloadInternalUsers(u []conf.AuthInternalUser) { |
||||||
|
m.mutex.Lock() |
||||||
|
defer m.mutex.Unlock() |
||||||
|
m.InternalUsers = u |
||||||
|
} |
||||||
|
|
||||||
|
// 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 { |
||||||
|
err := rtspAuthHeader.Unmarshal(req.RTSPRequest.Header["Authorization"]) |
||||||
|
if err == nil { |
||||||
|
switch rtspAuthHeader.Method { |
||||||
|
case headers.AuthBasic: |
||||||
|
req.User = rtspAuthHeader.BasicUser |
||||||
|
req.Pass = rtspAuthHeader.BasicPass |
||||||
|
|
||||||
|
case headers.AuthDigestMD5: |
||||||
|
req.User = rtspAuthHeader.Username |
||||||
|
|
||||||
|
default: |
||||||
|
return fmt.Errorf("unsupported RTSP authentication method") |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
switch m.Method { |
||||||
|
case conf.AuthMethodInternal: |
||||||
|
return m.authenticateInternal(req, &rtspAuthHeader) |
||||||
|
|
||||||
|
case conf.AuthMethodHTTP: |
||||||
|
return m.authenticateHTTP(req) |
||||||
|
|
||||||
|
default: |
||||||
|
return m.authenticateJWT(req) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (m *Manager) authenticateInternal(req *Request, rtspAuthHeader *headers.Authorization) error { |
||||||
|
m.mutex.RLock() |
||||||
|
defer m.mutex.RUnlock() |
||||||
|
|
||||||
|
for _, u := range m.InternalUsers { |
||||||
|
if err := m.authenticateWithUser(req, rtspAuthHeader, &u); err == nil { |
||||||
|
return nil |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return fmt.Errorf("authentication failed") |
||||||
|
} |
||||||
|
|
||||||
|
func (m *Manager) authenticateWithUser( |
||||||
|
req *Request, |
||||||
|
rtspAuthHeader *headers.Authorization, |
||||||
|
u *conf.AuthInternalUser, |
||||||
|
) error { |
||||||
|
if u.User != "any" && !u.User.Check(req.User) { |
||||||
|
return fmt.Errorf("wrong user") |
||||||
|
} |
||||||
|
|
||||||
|
if len(u.IPs) != 0 && !u.IPs.Contains(req.IP) { |
||||||
|
return fmt.Errorf("IP not allowed") |
||||||
|
} |
||||||
|
|
||||||
|
if !matchesPermission(u.Permissions, req) { |
||||||
|
return fmt.Errorf("user doesn't have permission to perform action") |
||||||
|
} |
||||||
|
|
||||||
|
if u.User != "any" { |
||||||
|
if req.RTSPRequest != nil && rtspAuthHeader.Method == headers.AuthDigestMD5 { |
||||||
|
err := auth.Validate( |
||||||
|
req.RTSPRequest, |
||||||
|
string(u.User), |
||||||
|
string(u.Pass), |
||||||
|
req.RTSPBaseURL, |
||||||
|
m.RTSPAuthMethods, |
||||||
|
rtspAuthRealm, |
||||||
|
req.RTSPNonce) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
} else if !u.Pass.Check(req.Pass) { |
||||||
|
return fmt.Errorf("invalid credentials") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (m *Manager) authenticateHTTP(req *Request) error { |
||||||
|
if matchesPermission(m.HTTPExclude, req) { |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
enc, _ := json.Marshal(struct { |
||||||
|
IP string `json:"ip"` |
||||||
|
User string `json:"user"` |
||||||
|
Password string `json:"password"` |
||||||
|
Action string `json:"action"` |
||||||
|
Path string `json:"path"` |
||||||
|
Protocol string `json:"protocol"` |
||||||
|
ID *uuid.UUID `json:"id"` |
||||||
|
Query string `json:"query"` |
||||||
|
}{ |
||||||
|
IP: req.IP.String(), |
||||||
|
User: req.User, |
||||||
|
Password: req.Pass, |
||||||
|
Action: string(req.Action), |
||||||
|
Path: req.Path, |
||||||
|
Protocol: string(req.Protocol), |
||||||
|
ID: req.ID, |
||||||
|
Query: req.Query, |
||||||
|
}) |
||||||
|
|
||||||
|
res, err := http.Post(m.HTTPAddress, "application/json", bytes.NewReader(enc)) |
||||||
|
if err != nil { |
||||||
|
return fmt.Errorf("HTTP request failed: %w", err) |
||||||
|
} |
||||||
|
defer res.Body.Close() |
||||||
|
|
||||||
|
if res.StatusCode < 200 || res.StatusCode > 299 { |
||||||
|
if resBody, err := io.ReadAll(res.Body); err == nil && len(resBody) != 0 { |
||||||
|
return fmt.Errorf("server replied with code %d: %s", res.StatusCode, string(resBody)) |
||||||
|
} |
||||||
|
|
||||||
|
return fmt.Errorf("server replied with code %d", res.StatusCode) |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (m *Manager) authenticateJWT(req *Request) error { |
||||||
|
keyfunc, err := m.pullJWTJWKS() |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
v, err := url.ParseQuery(req.Query) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
if len(v["jwt"]) != 1 { |
||||||
|
return fmt.Errorf("JWT not provided") |
||||||
|
} |
||||||
|
|
||||||
|
var customClaims customClaims |
||||||
|
_, err = jwt.ParseWithClaims(v["jwt"][0], &customClaims, keyfunc) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
if !matchesPermission(customClaims.MediaMTXPermissions, req) { |
||||||
|
return fmt.Errorf("user doesn't have permission to perform action") |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (m *Manager) pullJWTJWKS() (jwt.Keyfunc, error) { |
||||||
|
now := time.Now() |
||||||
|
|
||||||
|
m.mutex.Lock() |
||||||
|
defer m.mutex.Unlock() |
||||||
|
|
||||||
|
if now.Sub(m.jwtLastRefresh) >= jwtRefreshPeriod { |
||||||
|
if m.jwtHTTPClient == nil { |
||||||
|
m.jwtHTTPClient = &http.Client{ |
||||||
|
Timeout: (m.ReadTimeout), |
||||||
|
Transport: &http.Transport{}, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
res, err := m.jwtHTTPClient.Get(m.JWTJWKS) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
defer res.Body.Close() |
||||||
|
|
||||||
|
var raw json.RawMessage |
||||||
|
err = json.NewDecoder(res.Body).Decode(&raw) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
tmp, err := keyfunc.NewJWKSetJSON(raw) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
m.jwtKeyFunc = tmp |
||||||
|
m.jwtLastRefresh = now |
||||||
|
} |
||||||
|
|
||||||
|
return m.jwtKeyFunc.Keyfunc, nil |
||||||
|
} |
||||||
@ -0,0 +1,309 @@ |
|||||||
|
package auth |
||||||
|
|
||||||
|
import ( |
||||||
|
"context" |
||||||
|
"encoding/json" |
||||||
|
"net" |
||||||
|
"net/http" |
||||||
|
"testing" |
||||||
|
|
||||||
|
"github.com/bluenviron/gortsplib/v4/pkg/auth" |
||||||
|
"github.com/bluenviron/gortsplib/v4/pkg/base" |
||||||
|
"github.com/bluenviron/gortsplib/v4/pkg/headers" |
||||||
|
"github.com/bluenviron/mediamtx/internal/conf" |
||||||
|
"github.com/stretchr/testify/require" |
||||||
|
) |
||||||
|
|
||||||
|
func mustParseCIDR(v string) net.IPNet { |
||||||
|
_, ne, err := net.ParseCIDR(v) |
||||||
|
if err != nil { |
||||||
|
panic(err) |
||||||
|
} |
||||||
|
if ipv4 := ne.IP.To4(); ipv4 != nil { |
||||||
|
return net.IPNet{IP: ipv4, Mask: ne.Mask[len(ne.Mask)-4 : len(ne.Mask)]} |
||||||
|
} |
||||||
|
return *ne |
||||||
|
} |
||||||
|
|
||||||
|
type testHTTPAuthenticator struct { |
||||||
|
*http.Server |
||||||
|
} |
||||||
|
|
||||||
|
func (ts *testHTTPAuthenticator) initialize(t *testing.T, protocol string, action string) { |
||||||
|
firstReceived := false |
||||||
|
|
||||||
|
ts.Server = &http.Server{ |
||||||
|
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
||||||
|
require.Equal(t, http.MethodPost, r.Method) |
||||||
|
require.Equal(t, "/auth", r.URL.Path) |
||||||
|
|
||||||
|
var in struct { |
||||||
|
IP string `json:"ip"` |
||||||
|
User string `json:"user"` |
||||||
|
Password string `json:"password"` |
||||||
|
Path string `json:"path"` |
||||||
|
Protocol string `json:"protocol"` |
||||||
|
ID string `json:"id"` |
||||||
|
Action string `json:"action"` |
||||||
|
Query string `json:"query"` |
||||||
|
} |
||||||
|
err := json.NewDecoder(r.Body).Decode(&in) |
||||||
|
require.NoError(t, err) |
||||||
|
|
||||||
|
var user string |
||||||
|
if action == "publish" { |
||||||
|
user = "testpublisher" |
||||||
|
} else { |
||||||
|
user = "testreader" |
||||||
|
} |
||||||
|
|
||||||
|
if in.IP != "127.0.0.1" || |
||||||
|
in.User != user || |
||||||
|
in.Password != "testpass" || |
||||||
|
in.Path != "teststream" || |
||||||
|
in.Protocol != protocol || |
||||||
|
(firstReceived && in.ID == "") || |
||||||
|
in.Action != action || |
||||||
|
(in.Query != "user=testreader&pass=testpass¶m=value" && |
||||||
|
in.Query != "user=testpublisher&pass=testpass¶m=value" && |
||||||
|
in.Query != "param=value") { |
||||||
|
w.WriteHeader(http.StatusBadRequest) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
firstReceived = true |
||||||
|
}), |
||||||
|
} |
||||||
|
|
||||||
|
ln, err := net.Listen("tcp", "127.0.0.1:9120") |
||||||
|
require.NoError(t, err) |
||||||
|
|
||||||
|
go ts.Server.Serve(ln) |
||||||
|
} |
||||||
|
|
||||||
|
func (ts *testHTTPAuthenticator) close() { |
||||||
|
ts.Server.Shutdown(context.Background()) |
||||||
|
} |
||||||
|
|
||||||
|
func TestAuthInternal(t *testing.T) { |
||||||
|
for _, outcome := range []string{ |
||||||
|
"ok", |
||||||
|
"wrong user", |
||||||
|
"wrong pass", |
||||||
|
"wrong ip", |
||||||
|
"wrong action", |
||||||
|
"wrong path", |
||||||
|
} { |
||||||
|
for _, encryption := range []string{ |
||||||
|
"plain", |
||||||
|
"sha256", |
||||||
|
"argon2", |
||||||
|
} { |
||||||
|
t.Run(outcome+" "+encryption, func(t *testing.T) { |
||||||
|
m := Manager{ |
||||||
|
Method: conf.AuthMethodInternal, |
||||||
|
InternalUsers: []conf.AuthInternalUser{ |
||||||
|
{ |
||||||
|
IPs: conf.IPNetworks{mustParseCIDR("127.1.1.1/32")}, |
||||||
|
Permissions: []conf.AuthInternalUserPermission{ |
||||||
|
{ |
||||||
|
Action: conf.AuthActionPublish, |
||||||
|
Path: "mypath", |
||||||
|
}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
HTTPAddress: "", |
||||||
|
RTSPAuthMethods: nil, |
||||||
|
} |
||||||
|
|
||||||
|
switch encryption { |
||||||
|
case "plain": |
||||||
|
m.InternalUsers[0].User = conf.Credential("testuser") |
||||||
|
m.InternalUsers[0].Pass = conf.Credential("testpass") |
||||||
|
|
||||||
|
case "sha256": |
||||||
|
m.InternalUsers[0].User = conf.Credential("sha256:rl3rgi4NcZkpAEcacZnQ2VuOfJ0FxAqCRaKB/SwdZoQ=") |
||||||
|
m.InternalUsers[0].Pass = conf.Credential("sha256:E9JJ8stBJ7QM+nV4ZoUCeHk/gU3tPFh/5YieiJp6n2w=") |
||||||
|
|
||||||
|
case "argon2": |
||||||
|
m.InternalUsers[0].User = conf.Credential( |
||||||
|
"argon2:$argon2id$v=19$m=4096,t=3,p=1$MTIzNDU2Nzg$Ux/LWeTgJQPyfMMJo1myR64+o8rALHoPmlE1i/TR+58") |
||||||
|
m.InternalUsers[0].Pass = conf.Credential( |
||||||
|
"argon2:$argon2i$v=19$m=4096,t=3,p=1$MTIzNDU2Nzg$/mrZ42TiTv1mcPnpMUera5oi0SFYbbyueAbdx5sUvWo") |
||||||
|
} |
||||||
|
|
||||||
|
switch outcome { |
||||||
|
case "ok": |
||||||
|
err := m.Authenticate(&Request{ |
||||||
|
User: "testuser", |
||||||
|
Pass: "testpass", |
||||||
|
IP: net.ParseIP("127.1.1.1"), |
||||||
|
Action: conf.AuthActionPublish, |
||||||
|
Path: "mypath", |
||||||
|
}) |
||||||
|
require.NoError(t, err) |
||||||
|
|
||||||
|
case "wrong user": |
||||||
|
err := m.Authenticate(&Request{ |
||||||
|
User: "wrong", |
||||||
|
Pass: "testpass", |
||||||
|
IP: net.ParseIP("127.1.1.1"), |
||||||
|
Action: conf.AuthActionPublish, |
||||||
|
Path: "mypath", |
||||||
|
}) |
||||||
|
require.Error(t, err) |
||||||
|
|
||||||
|
case "wrong pass": |
||||||
|
err := m.Authenticate(&Request{ |
||||||
|
User: "testuser", |
||||||
|
Pass: "wrong", |
||||||
|
IP: net.ParseIP("127.1.1.1"), |
||||||
|
Action: conf.AuthActionPublish, |
||||||
|
Path: "mypath", |
||||||
|
}) |
||||||
|
require.Error(t, err) |
||||||
|
|
||||||
|
case "wrong ip": |
||||||
|
err := m.Authenticate(&Request{ |
||||||
|
User: "testuser", |
||||||
|
Pass: "testpass", |
||||||
|
IP: net.ParseIP("127.1.1.2"), |
||||||
|
Action: conf.AuthActionPublish, |
||||||
|
Path: "mypath", |
||||||
|
}) |
||||||
|
require.Error(t, err) |
||||||
|
|
||||||
|
case "wrong action": |
||||||
|
err := m.Authenticate(&Request{ |
||||||
|
User: "testuser", |
||||||
|
Pass: "testpass", |
||||||
|
IP: net.ParseIP("127.1.1.1"), |
||||||
|
Action: conf.AuthActionRead, |
||||||
|
Path: "mypath", |
||||||
|
}) |
||||||
|
require.Error(t, err) |
||||||
|
|
||||||
|
case "wrong path": |
||||||
|
err := m.Authenticate(&Request{ |
||||||
|
User: "testuser", |
||||||
|
Pass: "testpass", |
||||||
|
IP: net.ParseIP("127.1.1.1"), |
||||||
|
Action: conf.AuthActionPublish, |
||||||
|
Path: "wrong", |
||||||
|
}) |
||||||
|
require.Error(t, err) |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func TestAuthInternalRTSPDigest(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: []headers.AuthMethod{headers.AuthDigestMD5}, |
||||||
|
} |
||||||
|
|
||||||
|
u, err := base.ParseURL("rtsp://127.0.0.1:8554/mypath") |
||||||
|
require.NoError(t, err) |
||||||
|
|
||||||
|
s, err := auth.NewSender( |
||||||
|
auth.GenerateWWWAuthenticate([]headers.AuthMethod{headers.AuthDigestMD5}, "IPCAM", "mynonce"), |
||||||
|
"myuser", |
||||||
|
"mypass", |
||||||
|
) |
||||||
|
require.NoError(t, err) |
||||||
|
|
||||||
|
req := &base.Request{ |
||||||
|
Method: "ANNOUNCE", |
||||||
|
URL: u, |
||||||
|
} |
||||||
|
|
||||||
|
s.AddAuthorization(req) |
||||||
|
|
||||||
|
err = m.Authenticate(&Request{ |
||||||
|
IP: net.ParseIP("127.1.1.1"), |
||||||
|
Action: conf.AuthActionPublish, |
||||||
|
Path: "mypath", |
||||||
|
RTSPRequest: req, |
||||||
|
RTSPNonce: "mynonce", |
||||||
|
}) |
||||||
|
require.NoError(t, err) |
||||||
|
} |
||||||
|
|
||||||
|
func TestAuthHTTP(t *testing.T) { |
||||||
|
for _, outcome := range []string{"ok", "fail"} { |
||||||
|
t.Run(outcome, func(t *testing.T) { |
||||||
|
m := Manager{ |
||||||
|
Method: conf.AuthMethodHTTP, |
||||||
|
HTTPAddress: "http://127.0.0.1:9120/auth", |
||||||
|
RTSPAuthMethods: nil, |
||||||
|
} |
||||||
|
|
||||||
|
au := &testHTTPAuthenticator{} |
||||||
|
au.initialize(t, "rtsp", "publish") |
||||||
|
defer au.close() |
||||||
|
|
||||||
|
if outcome == "ok" { |
||||||
|
err := m.Authenticate(&Request{ |
||||||
|
User: "testpublisher", |
||||||
|
Pass: "testpass", |
||||||
|
IP: net.ParseIP("127.0.0.1"), |
||||||
|
Action: conf.AuthActionPublish, |
||||||
|
Path: "teststream", |
||||||
|
Protocol: ProtocolRTSP, |
||||||
|
Query: "param=value", |
||||||
|
}) |
||||||
|
require.NoError(t, err) |
||||||
|
} else { |
||||||
|
err := m.Authenticate(&Request{ |
||||||
|
User: "invalid", |
||||||
|
Pass: "testpass", |
||||||
|
IP: net.ParseIP("127.0.0.1"), |
||||||
|
Action: conf.AuthActionPublish, |
||||||
|
Path: "teststream", |
||||||
|
Protocol: ProtocolRTSP, |
||||||
|
Query: "param=value", |
||||||
|
}) |
||||||
|
require.Error(t, err) |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func TestAuthHTTPExclude(t *testing.T) { |
||||||
|
m := Manager{ |
||||||
|
Method: conf.AuthMethodHTTP, |
||||||
|
HTTPAddress: "http://not-to-be-used:9120/auth", |
||||||
|
HTTPExclude: []conf.AuthInternalUserPermission{{ |
||||||
|
Action: conf.AuthActionPublish, |
||||||
|
}}, |
||||||
|
RTSPAuthMethods: nil, |
||||||
|
} |
||||||
|
|
||||||
|
err := m.Authenticate(&Request{ |
||||||
|
User: "", |
||||||
|
Pass: "", |
||||||
|
IP: net.ParseIP("127.0.0.1"), |
||||||
|
Action: conf.AuthActionPublish, |
||||||
|
Path: "teststream", |
||||||
|
Protocol: ProtocolRTSP, |
||||||
|
Query: "param=value", |
||||||
|
}) |
||||||
|
require.NoError(t, err) |
||||||
|
} |
||||||
@ -0,0 +1,52 @@ |
|||||||
|
package conf |
||||||
|
|
||||||
|
import ( |
||||||
|
"encoding/json" |
||||||
|
"fmt" |
||||||
|
) |
||||||
|
|
||||||
|
// AuthAction is an authentication action.
|
||||||
|
type AuthAction string |
||||||
|
|
||||||
|
// auth actions
|
||||||
|
const ( |
||||||
|
AuthActionPublish AuthAction = "publish" |
||||||
|
AuthActionRead AuthAction = "read" |
||||||
|
AuthActionPlayback AuthAction = "playback" |
||||||
|
AuthActionAPI AuthAction = "api" |
||||||
|
AuthActionMetrics AuthAction = "metrics" |
||||||
|
AuthActionPprof AuthAction = "pprof" |
||||||
|
) |
||||||
|
|
||||||
|
// MarshalJSON implements json.Marshaler.
|
||||||
|
func (d AuthAction) MarshalJSON() ([]byte, error) { |
||||||
|
return json.Marshal(string(d)) |
||||||
|
} |
||||||
|
|
||||||
|
// UnmarshalJSON implements json.Unmarshaler.
|
||||||
|
func (d *AuthAction) UnmarshalJSON(b []byte) error { |
||||||
|
var in string |
||||||
|
if err := json.Unmarshal(b, &in); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
switch in { |
||||||
|
case string(AuthActionPublish), |
||||||
|
string(AuthActionRead), |
||||||
|
string(AuthActionPlayback), |
||||||
|
string(AuthActionAPI), |
||||||
|
string(AuthActionMetrics), |
||||||
|
string(AuthActionPprof): |
||||||
|
*d = AuthAction(in) |
||||||
|
|
||||||
|
default: |
||||||
|
return fmt.Errorf("invalid auth action: '%s'", in) |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// UnmarshalEnv implements env.Unmarshaler.
|
||||||
|
func (d *AuthAction) UnmarshalEnv(_ string, v string) error { |
||||||
|
return d.UnmarshalJSON([]byte(`"` + v + `"`)) |
||||||
|
} |
||||||
@ -0,0 +1,15 @@ |
|||||||
|
package conf |
||||||
|
|
||||||
|
// AuthInternalUserPermission is a permission of a user.
|
||||||
|
type AuthInternalUserPermission struct { |
||||||
|
Action AuthAction `json:"action"` |
||||||
|
Path string `json:"path"` |
||||||
|
} |
||||||
|
|
||||||
|
// AuthInternalUser is an user.
|
||||||
|
type AuthInternalUser struct { |
||||||
|
User Credential `json:"user"` |
||||||
|
Pass Credential `json:"pass"` |
||||||
|
IPs IPNetworks `json:"ips"` |
||||||
|
Permissions []AuthInternalUserPermission `json:"permissions"` |
||||||
|
} |
||||||
@ -0,0 +1,84 @@ |
|||||||
|
package conf |
||||||
|
|
||||||
|
import ( |
||||||
|
"encoding/json" |
||||||
|
"fmt" |
||||||
|
"net" |
||||||
|
"sort" |
||||||
|
"strings" |
||||||
|
) |
||||||
|
|
||||||
|
// IPNetworks is a parameter that contains a list of IP networks.
|
||||||
|
type IPNetworks []net.IPNet |
||||||
|
|
||||||
|
// MarshalJSON implements json.Marshaler.
|
||||||
|
func (d IPNetworks) MarshalJSON() ([]byte, error) { |
||||||
|
out := make([]string, len(d)) |
||||||
|
|
||||||
|
for i, v := range d { |
||||||
|
out[i] = v.String() |
||||||
|
} |
||||||
|
|
||||||
|
sort.Strings(out) |
||||||
|
|
||||||
|
return json.Marshal(out) |
||||||
|
} |
||||||
|
|
||||||
|
// UnmarshalJSON implements json.Unmarshaler.
|
||||||
|
func (d *IPNetworks) UnmarshalJSON(b []byte) error { |
||||||
|
var in []string |
||||||
|
if err := json.Unmarshal(b, &in); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
*d = nil |
||||||
|
|
||||||
|
if len(in) == 0 { |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
for _, t := range in { |
||||||
|
if _, ipnet, err := net.ParseCIDR(t); err == nil { |
||||||
|
if ipv4 := ipnet.IP.To4(); ipv4 != nil { |
||||||
|
*d = append(*d, net.IPNet{IP: ipv4, Mask: ipnet.Mask[len(ipnet.Mask)-4 : len(ipnet.Mask)]}) |
||||||
|
} else { |
||||||
|
*d = append(*d, *ipnet) |
||||||
|
} |
||||||
|
} else if ip := net.ParseIP(t); ip != nil { |
||||||
|
if ipv4 := ip.To4(); ipv4 != nil { |
||||||
|
*d = append(*d, net.IPNet{IP: ipv4, Mask: net.CIDRMask(32, 32)}) |
||||||
|
} else { |
||||||
|
*d = append(*d, net.IPNet{IP: ip, Mask: net.CIDRMask(128, 128)}) |
||||||
|
} |
||||||
|
} else { |
||||||
|
return fmt.Errorf("unable to parse IP/CIDR '%s'", t) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// UnmarshalEnv implements env.Unmarshaler.
|
||||||
|
func (d *IPNetworks) UnmarshalEnv(_ string, v string) error { |
||||||
|
byts, _ := json.Marshal(strings.Split(v, ",")) |
||||||
|
return d.UnmarshalJSON(byts) |
||||||
|
} |
||||||
|
|
||||||
|
// ToTrustedProxies converts IPNetworks into a string slice for SetTrustedProxies.
|
||||||
|
func (d *IPNetworks) ToTrustedProxies() []string { |
||||||
|
ret := make([]string, len(*d)) |
||||||
|
for i, entry := range *d { |
||||||
|
ret[i] = entry.String() |
||||||
|
} |
||||||
|
return ret |
||||||
|
} |
||||||
|
|
||||||
|
// Contains checks whether the IP is part of one of the networks.
|
||||||
|
func (d IPNetworks) Contains(ip net.IP) bool { |
||||||
|
for _, network := range d { |
||||||
|
if network.Contains(ip) { |
||||||
|
return true |
||||||
|
} |
||||||
|
} |
||||||
|
return false |
||||||
|
} |
||||||
@ -1,66 +0,0 @@ |
|||||||
package conf |
|
||||||
|
|
||||||
import ( |
|
||||||
"encoding/json" |
|
||||||
"fmt" |
|
||||||
"net" |
|
||||||
"sort" |
|
||||||
"strings" |
|
||||||
) |
|
||||||
|
|
||||||
// IPsOrCIDRs is a parameter that contains a list of IPs or CIDRs.
|
|
||||||
type IPsOrCIDRs []fmt.Stringer |
|
||||||
|
|
||||||
// MarshalJSON implements json.Marshaler.
|
|
||||||
func (d IPsOrCIDRs) MarshalJSON() ([]byte, error) { |
|
||||||
out := make([]string, len(d)) |
|
||||||
|
|
||||||
for i, v := range d { |
|
||||||
out[i] = v.String() |
|
||||||
} |
|
||||||
|
|
||||||
sort.Strings(out) |
|
||||||
|
|
||||||
return json.Marshal(out) |
|
||||||
} |
|
||||||
|
|
||||||
// UnmarshalJSON implements json.Unmarshaler.
|
|
||||||
func (d *IPsOrCIDRs) UnmarshalJSON(b []byte) error { |
|
||||||
var in []string |
|
||||||
if err := json.Unmarshal(b, &in); err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
*d = nil |
|
||||||
|
|
||||||
if len(in) == 0 { |
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
for _, t := range in { |
|
||||||
if _, ipnet, err := net.ParseCIDR(t); err == nil { |
|
||||||
*d = append(*d, ipnet) |
|
||||||
} else if ip := net.ParseIP(t); ip != nil { |
|
||||||
*d = append(*d, ip) |
|
||||||
} else { |
|
||||||
return fmt.Errorf("unable to parse IP/CIDR '%s'", t) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
// UnmarshalEnv implements env.Unmarshaler.
|
|
||||||
func (d *IPsOrCIDRs) UnmarshalEnv(_ string, v string) error { |
|
||||||
byts, _ := json.Marshal(strings.Split(v, ",")) |
|
||||||
return d.UnmarshalJSON(byts) |
|
||||||
} |
|
||||||
|
|
||||||
// ToTrustedProxies converts IPsOrCIDRs into a string slice for SetTrustedProxies.
|
|
||||||
func (d *IPsOrCIDRs) ToTrustedProxies() []string { |
|
||||||
ret := make([]string, len(*d)) |
|
||||||
for i, entry := range *d { |
|
||||||
ret[i] = entry.String() |
|
||||||
} |
|
||||||
return ret |
|
||||||
} |
|
||||||
@ -0,0 +1,63 @@ |
|||||||
|
package conf |
||||||
|
|
||||||
|
import ( |
||||||
|
"encoding/json" |
||||||
|
"fmt" |
||||||
|
"sort" |
||||||
|
"strings" |
||||||
|
|
||||||
|
"github.com/bluenviron/gortsplib/v4/pkg/headers" |
||||||
|
) |
||||||
|
|
||||||
|
// RTSPAuthMethods is the rtspAuthMethods parameter.
|
||||||
|
type RTSPAuthMethods []headers.AuthMethod |
||||||
|
|
||||||
|
// MarshalJSON implements json.Marshaler.
|
||||||
|
func (d RTSPAuthMethods) MarshalJSON() ([]byte, error) { |
||||||
|
out := make([]string, len(d)) |
||||||
|
|
||||||
|
for i, v := range d { |
||||||
|
switch v { |
||||||
|
case headers.AuthBasic: |
||||||
|
out[i] = "basic" |
||||||
|
|
||||||
|
default: |
||||||
|
out[i] = "digest" |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
sort.Strings(out) |
||||||
|
|
||||||
|
return json.Marshal(out) |
||||||
|
} |
||||||
|
|
||||||
|
// UnmarshalJSON implements json.Unmarshaler.
|
||||||
|
func (d *RTSPAuthMethods) UnmarshalJSON(b []byte) error { |
||||||
|
var in []string |
||||||
|
if err := json.Unmarshal(b, &in); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
*d = nil |
||||||
|
|
||||||
|
for _, v := range in { |
||||||
|
switch v { |
||||||
|
case "basic": |
||||||
|
*d = append(*d, headers.AuthBasic) |
||||||
|
|
||||||
|
case "digest": |
||||||
|
*d = append(*d, headers.AuthDigestMD5) |
||||||
|
|
||||||
|
default: |
||||||
|
return fmt.Errorf("invalid authentication method: '%s'", v) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// UnmarshalEnv implements env.Unmarshaler.
|
||||||
|
func (d *RTSPAuthMethods) UnmarshalEnv(_ string, v string) error { |
||||||
|
byts, _ := json.Marshal(strings.Split(v, ",")) |
||||||
|
return d.UnmarshalJSON(byts) |
||||||
|
} |
||||||
@ -1,126 +0,0 @@ |
|||||||
package core |
|
||||||
|
|
||||||
import ( |
|
||||||
"bytes" |
|
||||||
"encoding/json" |
|
||||||
"fmt" |
|
||||||
"io" |
|
||||||
"net/http" |
|
||||||
|
|
||||||
"github.com/bluenviron/gortsplib/v4/pkg/auth" |
|
||||||
"github.com/bluenviron/gortsplib/v4/pkg/headers" |
|
||||||
"github.com/google/uuid" |
|
||||||
|
|
||||||
"github.com/bluenviron/mediamtx/internal/conf" |
|
||||||
"github.com/bluenviron/mediamtx/internal/defs" |
|
||||||
) |
|
||||||
|
|
||||||
func doExternalAuthentication( |
|
||||||
ur string, |
|
||||||
accessRequest defs.PathAccessRequest, |
|
||||||
) error { |
|
||||||
enc, _ := json.Marshal(struct { |
|
||||||
IP string `json:"ip"` |
|
||||||
User string `json:"user"` |
|
||||||
Password string `json:"password"` |
|
||||||
Path string `json:"path"` |
|
||||||
Protocol string `json:"protocol"` |
|
||||||
ID *uuid.UUID `json:"id"` |
|
||||||
Action string `json:"action"` |
|
||||||
Query string `json:"query"` |
|
||||||
}{ |
|
||||||
IP: accessRequest.IP.String(), |
|
||||||
User: accessRequest.User, |
|
||||||
Password: accessRequest.Pass, |
|
||||||
Path: accessRequest.Name, |
|
||||||
Protocol: string(accessRequest.Proto), |
|
||||||
ID: accessRequest.ID, |
|
||||||
Action: func() string { |
|
||||||
if accessRequest.Publish { |
|
||||||
return "publish" |
|
||||||
} |
|
||||||
return "read" |
|
||||||
}(), |
|
||||||
Query: accessRequest.Query, |
|
||||||
}) |
|
||||||
res, err := http.Post(ur, "application/json", bytes.NewReader(enc)) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
defer res.Body.Close() |
|
||||||
|
|
||||||
if res.StatusCode < 200 || res.StatusCode > 299 { |
|
||||||
if resBody, err := io.ReadAll(res.Body); err == nil && len(resBody) != 0 { |
|
||||||
return fmt.Errorf("server replied with code %d: %s", res.StatusCode, string(resBody)) |
|
||||||
} |
|
||||||
return fmt.Errorf("server replied with code %d", res.StatusCode) |
|
||||||
} |
|
||||||
|
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
func doAuthentication( |
|
||||||
externalAuthenticationURL string, |
|
||||||
rtspAuthMethods conf.AuthMethods, |
|
||||||
pathConf *conf.Path, |
|
||||||
accessRequest defs.PathAccessRequest, |
|
||||||
) error { |
|
||||||
var rtspAuth headers.Authorization |
|
||||||
if accessRequest.RTSPRequest != nil { |
|
||||||
err := rtspAuth.Unmarshal(accessRequest.RTSPRequest.Header["Authorization"]) |
|
||||||
if err == nil && rtspAuth.Method == headers.AuthBasic { |
|
||||||
accessRequest.User = rtspAuth.BasicUser |
|
||||||
accessRequest.Pass = rtspAuth.BasicPass |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
if externalAuthenticationURL != "" { |
|
||||||
err := doExternalAuthentication( |
|
||||||
externalAuthenticationURL, |
|
||||||
accessRequest, |
|
||||||
) |
|
||||||
if err != nil { |
|
||||||
return defs.AuthenticationError{Message: fmt.Sprintf("external authentication failed: %s", err)} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
var pathIPs conf.IPsOrCIDRs |
|
||||||
var pathUser conf.Credential |
|
||||||
var pathPass conf.Credential |
|
||||||
|
|
||||||
if accessRequest.Publish { |
|
||||||
pathIPs = pathConf.PublishIPs |
|
||||||
pathUser = pathConf.PublishUser |
|
||||||
pathPass = pathConf.PublishPass |
|
||||||
} else { |
|
||||||
pathIPs = pathConf.ReadIPs |
|
||||||
pathUser = pathConf.ReadUser |
|
||||||
pathPass = pathConf.ReadPass |
|
||||||
} |
|
||||||
|
|
||||||
if pathIPs != nil { |
|
||||||
if !ipEqualOrInRange(accessRequest.IP, pathIPs) { |
|
||||||
return defs.AuthenticationError{Message: fmt.Sprintf("IP %s not allowed", accessRequest.IP)} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
if pathUser != "" { |
|
||||||
if accessRequest.RTSPRequest != nil && rtspAuth.Method == headers.AuthDigestMD5 { |
|
||||||
err := auth.Validate( |
|
||||||
accessRequest.RTSPRequest, |
|
||||||
string(pathUser), |
|
||||||
string(pathPass), |
|
||||||
accessRequest.RTSPBaseURL, |
|
||||||
rtspAuthMethods, |
|
||||||
"IPCAM", |
|
||||||
accessRequest.RTSPNonce) |
|
||||||
if err != nil { |
|
||||||
return defs.AuthenticationError{Message: err.Error()} |
|
||||||
} |
|
||||||
} else if !pathUser.Check(accessRequest.User) || !pathPass.Check(accessRequest.Pass) { |
|
||||||
return defs.AuthenticationError{Message: "invalid credentials"} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
return nil |
|
||||||
} |
|
||||||
@ -1,155 +0,0 @@ |
|||||||
package core |
|
||||||
|
|
||||||
import ( |
|
||||||
"context" |
|
||||||
"encoding/json" |
|
||||||
"net" |
|
||||||
"net/http" |
|
||||||
"testing" |
|
||||||
|
|
||||||
"github.com/bluenviron/gortsplib/v4/pkg/headers" |
|
||||||
"github.com/bluenviron/mediamtx/internal/conf" |
|
||||||
"github.com/bluenviron/mediamtx/internal/defs" |
|
||||||
"github.com/stretchr/testify/require" |
|
||||||
) |
|
||||||
|
|
||||||
type testHTTPAuthenticator struct { |
|
||||||
*http.Server |
|
||||||
} |
|
||||||
|
|
||||||
func (ts *testHTTPAuthenticator) initialize(t *testing.T, protocol string, action string) { |
|
||||||
firstReceived := false |
|
||||||
|
|
||||||
ts.Server = &http.Server{ |
|
||||||
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
|
||||||
require.Equal(t, http.MethodPost, r.Method) |
|
||||||
require.Equal(t, "/auth", r.URL.Path) |
|
||||||
|
|
||||||
var in struct { |
|
||||||
IP string `json:"ip"` |
|
||||||
User string `json:"user"` |
|
||||||
Password string `json:"password"` |
|
||||||
Path string `json:"path"` |
|
||||||
Protocol string `json:"protocol"` |
|
||||||
ID string `json:"id"` |
|
||||||
Action string `json:"action"` |
|
||||||
Query string `json:"query"` |
|
||||||
} |
|
||||||
err := json.NewDecoder(r.Body).Decode(&in) |
|
||||||
require.NoError(t, err) |
|
||||||
|
|
||||||
var user string |
|
||||||
if action == "publish" { |
|
||||||
user = "testpublisher" |
|
||||||
} else { |
|
||||||
user = "testreader" |
|
||||||
} |
|
||||||
|
|
||||||
if in.IP != "127.0.0.1" || |
|
||||||
in.User != user || |
|
||||||
in.Password != "testpass" || |
|
||||||
in.Path != "teststream" || |
|
||||||
in.Protocol != protocol || |
|
||||||
(firstReceived && in.ID == "") || |
|
||||||
in.Action != action || |
|
||||||
(in.Query != "user=testreader&pass=testpass¶m=value" && |
|
||||||
in.Query != "user=testpublisher&pass=testpass¶m=value" && |
|
||||||
in.Query != "param=value") { |
|
||||||
w.WriteHeader(http.StatusBadRequest) |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
firstReceived = true |
|
||||||
}), |
|
||||||
} |
|
||||||
|
|
||||||
ln, err := net.Listen("tcp", "127.0.0.1:9120") |
|
||||||
require.NoError(t, err) |
|
||||||
|
|
||||||
go ts.Server.Serve(ln) |
|
||||||
} |
|
||||||
|
|
||||||
func (ts *testHTTPAuthenticator) close() { |
|
||||||
ts.Server.Shutdown(context.Background()) |
|
||||||
} |
|
||||||
|
|
||||||
func TestAuthSha256(t *testing.T) { |
|
||||||
err := doAuthentication( |
|
||||||
"", |
|
||||||
conf.AuthMethods{headers.AuthBasic}, |
|
||||||
&conf.Path{ |
|
||||||
PublishUser: conf.Credential("sha256:rl3rgi4NcZkpAEcacZnQ2VuOfJ0FxAqCRaKB/SwdZoQ="), |
|
||||||
PublishPass: conf.Credential("sha256:E9JJ8stBJ7QM+nV4ZoUCeHk/gU3tPFh/5YieiJp6n2w="), |
|
||||||
}, |
|
||||||
defs.PathAccessRequest{ |
|
||||||
Name: "mypath", |
|
||||||
Query: "", |
|
||||||
Publish: true, |
|
||||||
SkipAuth: false, |
|
||||||
IP: net.ParseIP("127.0.0.1"), |
|
||||||
User: "testuser", |
|
||||||
Pass: "testpass", |
|
||||||
Proto: defs.AuthProtocolRTSP, |
|
||||||
ID: nil, |
|
||||||
RTSPRequest: nil, |
|
||||||
RTSPBaseURL: nil, |
|
||||||
RTSPNonce: "", |
|
||||||
}, |
|
||||||
) |
|
||||||
require.NoError(t, err) |
|
||||||
} |
|
||||||
|
|
||||||
func TestAuthArgon2(t *testing.T) { |
|
||||||
err := doAuthentication( |
|
||||||
"", |
|
||||||
conf.AuthMethods{headers.AuthBasic}, |
|
||||||
&conf.Path{ |
|
||||||
PublishUser: conf.Credential( |
|
||||||
"argon2:$argon2id$v=19$m=4096,t=3,p=1$MTIzNDU2Nzg$Ux/LWeTgJQPyfMMJo1myR64+o8rALHoPmlE1i/TR+58"), |
|
||||||
PublishPass: conf.Credential( |
|
||||||
"argon2:$argon2i$v=19$m=4096,t=3,p=1$MTIzNDU2Nzg$/mrZ42TiTv1mcPnpMUera5oi0SFYbbyueAbdx5sUvWo"), |
|
||||||
}, |
|
||||||
defs.PathAccessRequest{ |
|
||||||
Name: "mypath", |
|
||||||
Query: "", |
|
||||||
Publish: true, |
|
||||||
SkipAuth: false, |
|
||||||
IP: net.ParseIP("127.0.0.1"), |
|
||||||
User: "testuser", |
|
||||||
Pass: "testpass", |
|
||||||
Proto: defs.AuthProtocolRTSP, |
|
||||||
ID: nil, |
|
||||||
RTSPRequest: nil, |
|
||||||
RTSPBaseURL: nil, |
|
||||||
RTSPNonce: "", |
|
||||||
}, |
|
||||||
) |
|
||||||
require.NoError(t, err) |
|
||||||
} |
|
||||||
|
|
||||||
func TestAuthExternal(t *testing.T) { |
|
||||||
au := &testHTTPAuthenticator{} |
|
||||||
au.initialize(t, "rtsp", "publish") |
|
||||||
defer au.close() |
|
||||||
|
|
||||||
err := doAuthentication( |
|
||||||
"http://127.0.0.1:9120/auth", |
|
||||||
conf.AuthMethods{headers.AuthBasic}, |
|
||||||
&conf.Path{}, |
|
||||||
defs.PathAccessRequest{ |
|
||||||
Name: "teststream", |
|
||||||
Query: "param=value", |
|
||||||
Publish: true, |
|
||||||
SkipAuth: false, |
|
||||||
IP: net.ParseIP("127.0.0.1"), |
|
||||||
User: "testpublisher", |
|
||||||
Pass: "testpass", |
|
||||||
Proto: defs.AuthProtocolRTSP, |
|
||||||
ID: nil, |
|
||||||
RTSPRequest: nil, |
|
||||||
RTSPBaseURL: nil, |
|
||||||
RTSPNonce: "", |
|
||||||
}, |
|
||||||
) |
|
||||||
require.NoError(t, err) |
|
||||||
} |
|
||||||
@ -1,23 +0,0 @@ |
|||||||
package core |
|
||||||
|
|
||||||
import ( |
|
||||||
"fmt" |
|
||||||
"net" |
|
||||||
) |
|
||||||
|
|
||||||
func ipEqualOrInRange(ip net.IP, ips []fmt.Stringer) bool { |
|
||||||
for _, item := range ips { |
|
||||||
switch titem := item.(type) { |
|
||||||
case net.IP: |
|
||||||
if titem.Equal(ip) { |
|
||||||
return true |
|
||||||
} |
|
||||||
|
|
||||||
case *net.IPNet: |
|
||||||
if titem.Contains(ip) { |
|
||||||
return true |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
return false |
|
||||||
} |
|
||||||
@ -1,23 +0,0 @@ |
|||||||
package defs |
|
||||||
|
|
||||||
// AuthProtocol is a authentication protocol.
|
|
||||||
type AuthProtocol string |
|
||||||
|
|
||||||
// authentication protocols.
|
|
||||||
const ( |
|
||||||
AuthProtocolRTSP AuthProtocol = "rtsp" |
|
||||||
AuthProtocolRTMP AuthProtocol = "rtmp" |
|
||||||
AuthProtocolHLS AuthProtocol = "hls" |
|
||||||
AuthProtocolWebRTC AuthProtocol = "webrtc" |
|
||||||
AuthProtocolSRT AuthProtocol = "srt" |
|
||||||
) |
|
||||||
|
|
||||||
// AuthenticationError is a authentication error.
|
|
||||||
type AuthenticationError struct { |
|
||||||
Message string |
|
||||||
} |
|
||||||
|
|
||||||
// Error implements the error interface.
|
|
||||||
func (e AuthenticationError) Error() string { |
|
||||||
return "authentication failed: " + e.Message |
|
||||||
} |
|
||||||
Loading…
Reference in new issue