golanggohlsrtmpwebrtcmedia-serverobs-studiortcprtmp-proxyrtmp-serverrtprtsprtsp-proxyrtsp-relayrtsp-serversrtstreamingwebrtc-proxy
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
327 lines
7.2 KiB
327 lines
7.2 KiB
// 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 |
|
}
|
|
|