Browse Source

unify authentication mechanisms (#1775)

pull/1778/head
Alessandro Ros 3 years ago committed by GitHub
parent
commit
d8678cef90
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      go.mod
  2. 4
      go.sum
  3. 15
      internal/conf/conf.go
  4. 27
      internal/conf/path.go
  5. 5
      internal/conf/yaml/load.go
  6. 183
      internal/core/authentication.go
  7. 14
      internal/core/core.go
  8. 71
      internal/core/external_auth.go
  9. 83
      internal/core/hls_muxer.go
  10. 70
      internal/core/path.go
  11. 134
      internal/core/path_manager.go
  12. 149
      internal/core/rtmp_conn.go
  13. 62
      internal/core/rtmp_server.go
  14. 235
      internal/core/rtsp_conn.go
  15. 56
      internal/core/rtsp_server.go
  16. 15
      internal/core/rtsp_server_test.go
  17. 91
      internal/core/rtsp_session.go
  18. 9
      internal/core/webrtc_conn.go
  19. 161
      internal/core/webrtc_server.go
  20. 3
      internal/websocket/serverconn.go
  21. 5
      mediamtx.yml

2
go.mod

@ -8,7 +8,7 @@ require (
github.com/alecthomas/kong v0.7.1 github.com/alecthomas/kong v0.7.1
github.com/asticode/go-astits v1.11.0 github.com/asticode/go-astits v1.11.0
github.com/bluenviron/gohlslib v0.2.3 github.com/bluenviron/gohlslib v0.2.3
github.com/bluenviron/gortsplib/v3 v3.5.0 github.com/bluenviron/gortsplib/v3 v3.6.1
github.com/bluenviron/mediacommon v0.5.0 github.com/bluenviron/mediacommon v0.5.0
github.com/fsnotify/fsnotify v1.6.0 github.com/fsnotify/fsnotify v1.6.0
github.com/gin-gonic/gin v1.9.0 github.com/gin-gonic/gin v1.9.0

4
go.sum

@ -14,8 +14,8 @@ github.com/asticode/go-astits v1.11.0 h1:GTHUXht0ZXAJXsVbsLIcyfHr1Bchi4QQwMARw2Z
github.com/asticode/go-astits v1.11.0/go.mod h1:QSHmknZ51pf6KJdHKZHJTLlMegIrhega3LPWz3ND/iI= github.com/asticode/go-astits v1.11.0/go.mod h1:QSHmknZ51pf6KJdHKZHJTLlMegIrhega3LPWz3ND/iI=
github.com/bluenviron/gohlslib v0.2.3 h1:vZmpjh2qWHaCvwwha04tgu8Kz9p4CuSBRLayD2yf89A= github.com/bluenviron/gohlslib v0.2.3 h1:vZmpjh2qWHaCvwwha04tgu8Kz9p4CuSBRLayD2yf89A=
github.com/bluenviron/gohlslib v0.2.3/go.mod h1:loD97sTtBh/nBcw8yZJgXc71A6XQb0FsDWXFRkl7Yj4= github.com/bluenviron/gohlslib v0.2.3/go.mod h1:loD97sTtBh/nBcw8yZJgXc71A6XQb0FsDWXFRkl7Yj4=
github.com/bluenviron/gortsplib/v3 v3.5.0 h1:8d6DYcwVhghObgBFOnoJwK6xf1ZiAQ8Vi7DRv6DGLdw= github.com/bluenviron/gortsplib/v3 v3.6.1 h1:+/kPiwmdRwUasU5thOBATJQ4/yD+vrIEutJyRTB/f+0=
github.com/bluenviron/gortsplib/v3 v3.5.0/go.mod h1:gc6Z8pBUMC9QBqYxcOY9eVxjDPOrmFcwVH61Xs3Gu2A= github.com/bluenviron/gortsplib/v3 v3.6.1/go.mod h1:gc6Z8pBUMC9QBqYxcOY9eVxjDPOrmFcwVH61Xs3Gu2A=
github.com/bluenviron/mediacommon v0.5.0 h1:YsVFlEknaXWhZGfz+Y1QbuzXLMVSmHODc7OnRqZoITY= github.com/bluenviron/mediacommon v0.5.0 h1:YsVFlEknaXWhZGfz+Y1QbuzXLMVSmHODc7OnRqZoITY=
github.com/bluenviron/mediacommon v0.5.0/go.mod h1:t0dqPsWUTchyvib0MhixIwXEgvDX4V9G+I0GzWLQRb8= github.com/bluenviron/mediacommon v0.5.0/go.mod h1:t0dqPsWUTchyvib0MhixIwXEgvDX4V9G+I0GzWLQRb8=
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=

15
internal/conf/conf.go

@ -195,6 +195,15 @@ func (conf Conf) Clone() *Conf {
return &dest return &dest
} }
func contains(list []headers.AuthMethod, item headers.AuthMethod) bool {
for _, i := range list {
if i == item {
return true
}
}
return false
}
// Check checks the configuration for errors. // Check checks the configuration for errors.
func (conf *Conf) Check() error { func (conf *Conf) Check() error {
// general // general
@ -209,6 +218,10 @@ func (conf *Conf) Check() error {
!strings.HasPrefix(conf.ExternalAuthenticationURL, "https://") { !strings.HasPrefix(conf.ExternalAuthenticationURL, "https://") {
return fmt.Errorf("'externalAuthenticationURL' must be a HTTP URL") return fmt.Errorf("'externalAuthenticationURL' must be a HTTP URL")
} }
if contains(conf.AuthMethods, headers.AuthDigest) {
return fmt.Errorf("'externalAuthenticationURL' can't be used when 'digest' is in authMethods")
}
} }
// RTSP // RTSP
@ -280,7 +293,7 @@ func (conf *Conf) UnmarshalJSON(b []byte) error {
conf.MulticastRTCPPort = 8003 conf.MulticastRTCPPort = 8003
conf.ServerKey = "server.key" conf.ServerKey = "server.key"
conf.ServerCert = "server.crt" conf.ServerCert = "server.crt"
conf.AuthMethods = AuthMethods{headers.AuthBasic, headers.AuthDigest} conf.AuthMethods = AuthMethods{headers.AuthBasic}
// RTMP // RTMP
conf.RTMPAddress = ":1935" conf.RTMPAddress = ":1935"

27
internal/conf/path.go

@ -11,6 +11,7 @@ import (
"strings" "strings"
"time" "time"
"github.com/bluenviron/gortsplib/v3/pkg/headers"
"github.com/bluenviron/gortsplib/v3/pkg/url" "github.com/bluenviron/gortsplib/v3/pkg/url"
) )
@ -252,30 +253,32 @@ func (pconf *PathConf) check(conf *Conf, name string) error {
"the stream is not provided by a publisher, but by a fixed source") "the stream is not provided by a publisher, but by a fixed source")
} }
if pconf.PublishUser != "" && conf.ExternalAuthenticationURL != "" {
return fmt.Errorf("'publishUser' can't be used with 'externalAuthenticationURL'")
}
if len(pconf.PublishIPs) > 0 && pconf.Source != "publisher" { if len(pconf.PublishIPs) > 0 && pconf.Source != "publisher" {
return fmt.Errorf("'publishIPs' is useless when source is not 'publisher', since " + return fmt.Errorf("'publishIPs' is useless when source is not 'publisher', since " +
"the stream is not provided by a publisher, but by a fixed source") "the stream is not provided by a publisher, but by a fixed source")
} }
if len(pconf.PublishIPs) > 0 && conf.ExternalAuthenticationURL != "" {
return fmt.Errorf("'publishIPs' can't be used with 'externalAuthenticationURL'")
}
if (pconf.ReadUser != "" && pconf.ReadPass == "") || if (pconf.ReadUser != "" && pconf.ReadPass == "") ||
(pconf.ReadUser == "" && pconf.ReadPass != "") { (pconf.ReadUser == "" && pconf.ReadPass != "") {
return fmt.Errorf("read username and password must be both filled") return fmt.Errorf("read username and password must be both filled")
} }
if pconf.ReadUser != "" && conf.ExternalAuthenticationURL != "" { if contains(conf.AuthMethods, headers.AuthDigest) {
return fmt.Errorf("'readUser' can't be used with 'externalAuthenticationURL'") if strings.HasPrefix(string(pconf.PublishUser), "sha256:") ||
strings.HasPrefix(string(pconf.PublishPass), "sha256:") ||
strings.HasPrefix(string(pconf.ReadUser), "sha256:") ||
strings.HasPrefix(string(pconf.ReadPass), "sha256:") {
return fmt.Errorf("hashed credentials can't be used when the digest auth method is available")
}
} }
if len(pconf.ReadIPs) > 0 && conf.ExternalAuthenticationURL != "" { if conf.ExternalAuthenticationURL != "" {
return fmt.Errorf("'readIPs' can't be used with 'externalAuthenticationURL'") if pconf.PublishUser != "" ||
len(pconf.PublishIPs) > 0 ||
pconf.ReadUser != "" ||
len(pconf.ReadIPs) > 0 {
return fmt.Errorf("credentials or IPs can't be used together with 'externalAuthenticationURL'")
}
} }
if pconf.RunOnInit != "" && pconf.Regexp != nil { if pconf.RunOnInit != "" && pconf.Regexp != nil {

5
internal/conf/yaml/load.go

@ -2,7 +2,6 @@
package yaml package yaml
import ( import (
"bytes"
"encoding/json" "encoding/json"
"fmt" "fmt"
@ -64,7 +63,5 @@ func Load(buf []byte, dest interface{}) error {
} }
// load JSON into destination // load JSON into destination
d := json.NewDecoder(bytes.NewReader(buf)) return json.Unmarshal(buf, dest)
d.DisallowUnknownFields()
return d.Decode(dest)
} }

183
internal/core/authentication.go

@ -0,0 +1,183 @@
package core
import (
"bytes"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"strings"
"github.com/bluenviron/gortsplib/v3/pkg/auth"
"github.com/bluenviron/gortsplib/v3/pkg/base"
"github.com/bluenviron/gortsplib/v3/pkg/headers"
"github.com/bluenviron/gortsplib/v3/pkg/url"
"github.com/google/uuid"
"github.com/aler9/mediamtx/internal/conf"
)
func sha256Base64(in string) string {
h := sha256.New()
h.Write([]byte(in))
return base64.StdEncoding.EncodeToString(h.Sum(nil))
}
func checkCredential(right string, guess string) bool {
if strings.HasPrefix(right, "sha256:") {
return right[len("sha256:"):] == sha256Base64(guess)
}
return right == guess
}
type authProtocol string
const (
authProtocolRTSP authProtocol = "rtsp"
authProtocolRTMP authProtocol = "rtmp"
authProtocolHLS authProtocol = "hls"
authProtocolWebRTC authProtocol = "webrtc"
)
func externalAuth(
ur string,
ip string,
user string,
password string,
path string,
protocol authProtocol,
id *uuid.UUID,
publish bool,
query string,
) 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: ip,
User: user,
Password: password,
Path: path,
Protocol: string(protocol),
Action: func() string {
if publish {
return "publish"
}
return "read"
}(),
Query: 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("external authentication replied with code %d: %s", res.StatusCode, string(resBody))
}
return fmt.Errorf("external authentication replied with code %d", res.StatusCode)
}
return nil
}
type authCredentials struct {
query string
ip net.IP
user string
pass string
proto authProtocol
id *uuid.UUID
rtspRequest *base.Request
rtspBaseURL *url.URL
rtspNonce string
}
func authenticate(
externalAuthenticationURL string,
rtspAuthMethods conf.AuthMethods,
pathName string,
pathConf *conf.PathConf,
publish bool,
credentials authCredentials,
) error {
var rtspAuth headers.Authorization
if credentials.rtspRequest != nil {
err := rtspAuth.Unmarshal(credentials.rtspRequest.Header["Authorization"])
if err == nil && rtspAuth.Method == headers.AuthBasic {
credentials.user = rtspAuth.BasicUser
credentials.pass = rtspAuth.BasicPass
}
}
if externalAuthenticationURL != "" {
err := externalAuth(
externalAuthenticationURL,
credentials.ip.String(),
credentials.user,
credentials.pass,
pathName,
credentials.proto,
credentials.id,
publish,
credentials.query,
)
if err != nil {
return fmt.Errorf("external authentication failed: %s", err)
}
}
var pathIPs conf.IPsOrCIDRs
var pathUser string
var pathPass string
if publish {
pathIPs = pathConf.PublishIPs
pathUser = string(pathConf.PublishUser)
pathPass = string(pathConf.PublishPass)
} else {
pathIPs = pathConf.ReadIPs
pathUser = string(pathConf.ReadUser)
pathPass = string(pathConf.ReadPass)
}
if pathIPs != nil {
if !ipEqualOrInRange(credentials.ip, pathIPs) {
return fmt.Errorf("IP '%s' not allowed", credentials.ip)
}
}
if pathUser != "" {
if credentials.rtspRequest != nil && rtspAuth.Method == headers.AuthDigest {
err := auth.Validate(
credentials.rtspRequest,
pathUser,
pathPass,
credentials.rtspBaseURL,
rtspAuthMethods,
"IPCAM",
credentials.rtspNonce)
if err != nil {
return err
}
} else if !checkCredential(pathUser, credentials.user) ||
!checkCredential(pathPass, credentials.pass) {
return fmt.Errorf("invalid credentials")
}
}
return nil
}

14
internal/core/core.go

@ -242,7 +242,9 @@ func (p *Core) createResources(initial bool) error {
if p.pathManager == nil { if p.pathManager == nil {
p.pathManager = newPathManager( p.pathManager = newPathManager(
p.ctx, p.ctx,
p.conf.ExternalAuthenticationURL,
p.conf.RTSPAddress, p.conf.RTSPAddress,
p.conf.AuthMethods,
p.conf.ReadTimeout, p.conf.ReadTimeout,
p.conf.WriteTimeout, p.conf.WriteTimeout,
p.conf.ReadBufferCount, p.conf.ReadBufferCount,
@ -262,7 +264,6 @@ func (p *Core) createResources(initial bool) error {
_, useMulticast := p.conf.Protocols[conf.Protocol(gortsplib.TransportUDPMulticast)] _, useMulticast := p.conf.Protocols[conf.Protocol(gortsplib.TransportUDPMulticast)]
p.rtspServer, err = newRTSPServer( p.rtspServer, err = newRTSPServer(
p.ctx, p.ctx,
p.conf.ExternalAuthenticationURL,
p.conf.RTSPAddress, p.conf.RTSPAddress,
p.conf.AuthMethods, p.conf.AuthMethods,
p.conf.ReadTimeout, p.conf.ReadTimeout,
@ -299,7 +300,6 @@ func (p *Core) createResources(initial bool) error {
if p.rtspsServer == nil { if p.rtspsServer == nil {
p.rtspsServer, err = newRTSPServer( p.rtspsServer, err = newRTSPServer(
p.ctx, p.ctx,
p.conf.ExternalAuthenticationURL,
p.conf.RTSPSAddress, p.conf.RTSPSAddress,
p.conf.AuthMethods, p.conf.AuthMethods,
p.conf.ReadTimeout, p.conf.ReadTimeout,
@ -336,7 +336,6 @@ func (p *Core) createResources(initial bool) error {
if p.rtmpServer == nil { if p.rtmpServer == nil {
p.rtmpServer, err = newRTMPServer( p.rtmpServer, err = newRTMPServer(
p.ctx, p.ctx,
p.conf.ExternalAuthenticationURL,
p.conf.RTMPAddress, p.conf.RTMPAddress,
p.conf.ReadTimeout, p.conf.ReadTimeout,
p.conf.WriteTimeout, p.conf.WriteTimeout,
@ -364,7 +363,6 @@ func (p *Core) createResources(initial bool) error {
if p.rtmpsServer == nil { if p.rtmpsServer == nil {
p.rtmpsServer, err = newRTMPServer( p.rtmpsServer, err = newRTMPServer(
p.ctx, p.ctx,
p.conf.ExternalAuthenticationURL,
p.conf.RTMPSAddress, p.conf.RTMPSAddress,
p.conf.ReadTimeout, p.conf.ReadTimeout,
p.conf.WriteTimeout, p.conf.WriteTimeout,
@ -420,7 +418,6 @@ func (p *Core) createResources(initial bool) error {
if p.webRTCServer == nil { if p.webRTCServer == nil {
p.webRTCServer, err = newWebRTCServer( p.webRTCServer, err = newWebRTCServer(
p.ctx, p.ctx,
p.conf.ExternalAuthenticationURL,
p.conf.WebRTCAddress, p.conf.WebRTCAddress,
p.conf.WebRTCEncryption, p.conf.WebRTCEncryption,
p.conf.WebRTCServerKey, p.conf.WebRTCServerKey,
@ -490,7 +487,9 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) {
newConf.ReadTimeout != p.conf.ReadTimeout newConf.ReadTimeout != p.conf.ReadTimeout
closePathManager := newConf == nil || closePathManager := newConf == nil ||
newConf.ExternalAuthenticationURL != p.conf.ExternalAuthenticationURL ||
newConf.RTSPAddress != p.conf.RTSPAddress || newConf.RTSPAddress != p.conf.RTSPAddress ||
!reflect.DeepEqual(newConf.AuthMethods, p.conf.AuthMethods) ||
newConf.ReadTimeout != p.conf.ReadTimeout || newConf.ReadTimeout != p.conf.ReadTimeout ||
newConf.WriteTimeout != p.conf.WriteTimeout || newConf.WriteTimeout != p.conf.WriteTimeout ||
newConf.ReadBufferCount != p.conf.ReadBufferCount || newConf.ReadBufferCount != p.conf.ReadBufferCount ||
@ -503,7 +502,6 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) {
closeRTSPServer := newConf == nil || closeRTSPServer := newConf == nil ||
newConf.RTSPDisable != p.conf.RTSPDisable || newConf.RTSPDisable != p.conf.RTSPDisable ||
newConf.Encryption != p.conf.Encryption || newConf.Encryption != p.conf.Encryption ||
newConf.ExternalAuthenticationURL != p.conf.ExternalAuthenticationURL ||
newConf.RTSPAddress != p.conf.RTSPAddress || newConf.RTSPAddress != p.conf.RTSPAddress ||
!reflect.DeepEqual(newConf.AuthMethods, p.conf.AuthMethods) || !reflect.DeepEqual(newConf.AuthMethods, p.conf.AuthMethods) ||
newConf.ReadTimeout != p.conf.ReadTimeout || newConf.ReadTimeout != p.conf.ReadTimeout ||
@ -525,7 +523,6 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) {
closeRTSPSServer := newConf == nil || closeRTSPSServer := newConf == nil ||
newConf.RTSPDisable != p.conf.RTSPDisable || newConf.RTSPDisable != p.conf.RTSPDisable ||
newConf.Encryption != p.conf.Encryption || newConf.Encryption != p.conf.Encryption ||
newConf.ExternalAuthenticationURL != p.conf.ExternalAuthenticationURL ||
newConf.RTSPSAddress != p.conf.RTSPSAddress || newConf.RTSPSAddress != p.conf.RTSPSAddress ||
!reflect.DeepEqual(newConf.AuthMethods, p.conf.AuthMethods) || !reflect.DeepEqual(newConf.AuthMethods, p.conf.AuthMethods) ||
newConf.ReadTimeout != p.conf.ReadTimeout || newConf.ReadTimeout != p.conf.ReadTimeout ||
@ -544,7 +541,6 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) {
newConf.RTMPDisable != p.conf.RTMPDisable || newConf.RTMPDisable != p.conf.RTMPDisable ||
newConf.RTMPEncryption != p.conf.RTMPEncryption || newConf.RTMPEncryption != p.conf.RTMPEncryption ||
newConf.RTMPAddress != p.conf.RTMPAddress || newConf.RTMPAddress != p.conf.RTMPAddress ||
newConf.ExternalAuthenticationURL != p.conf.ExternalAuthenticationURL ||
newConf.ReadTimeout != p.conf.ReadTimeout || newConf.ReadTimeout != p.conf.ReadTimeout ||
newConf.WriteTimeout != p.conf.WriteTimeout || newConf.WriteTimeout != p.conf.WriteTimeout ||
newConf.ReadBufferCount != p.conf.ReadBufferCount || newConf.ReadBufferCount != p.conf.ReadBufferCount ||
@ -558,7 +554,6 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) {
newConf.RTMPDisable != p.conf.RTMPDisable || newConf.RTMPDisable != p.conf.RTMPDisable ||
newConf.RTMPEncryption != p.conf.RTMPEncryption || newConf.RTMPEncryption != p.conf.RTMPEncryption ||
newConf.RTMPSAddress != p.conf.RTMPSAddress || newConf.RTMPSAddress != p.conf.RTMPSAddress ||
newConf.ExternalAuthenticationURL != p.conf.ExternalAuthenticationURL ||
newConf.ReadTimeout != p.conf.ReadTimeout || newConf.ReadTimeout != p.conf.ReadTimeout ||
newConf.WriteTimeout != p.conf.WriteTimeout || newConf.WriteTimeout != p.conf.WriteTimeout ||
newConf.ReadBufferCount != p.conf.ReadBufferCount || newConf.ReadBufferCount != p.conf.ReadBufferCount ||
@ -593,7 +588,6 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) {
closeWebRTCServer := newConf == nil || closeWebRTCServer := newConf == nil ||
newConf.WebRTCDisable != p.conf.WebRTCDisable || newConf.WebRTCDisable != p.conf.WebRTCDisable ||
newConf.ExternalAuthenticationURL != p.conf.ExternalAuthenticationURL ||
newConf.WebRTCAddress != p.conf.WebRTCAddress || newConf.WebRTCAddress != p.conf.WebRTCAddress ||
newConf.WebRTCEncryption != p.conf.WebRTCEncryption || newConf.WebRTCEncryption != p.conf.WebRTCEncryption ||
newConf.WebRTCServerKey != p.conf.WebRTCServerKey || newConf.WebRTCServerKey != p.conf.WebRTCServerKey ||

71
internal/core/external_auth.go

@ -1,71 +0,0 @@
package core
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"github.com/google/uuid"
)
type externalAuthProto string
const (
externalAuthProtoRTSP externalAuthProto = "rtsp"
externalAuthProtoRTMP externalAuthProto = "rtmp"
externalAuthProtoHLS externalAuthProto = "hls"
externalAuthProtoWebRTC externalAuthProto = "webrtc"
)
func externalAuth(
ur string,
ip string,
user string,
password string,
path string,
protocol externalAuthProto,
id *uuid.UUID,
publish bool,
query string,
) 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: ip,
User: user,
Password: password,
Path: path,
Protocol: string(protocol),
Action: func() string {
if publish {
return "publish"
}
return "read"
}(),
Query: 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 {
return fmt.Errorf("external authentication replied with code %d: %s", res.StatusCode, resBody)
}
return fmt.Errorf("external authentication replied with code %d", res.StatusCode)
}
return nil
}

83
internal/core/hls_muxer.go

@ -267,6 +267,7 @@ func (m *hlsMuxer) runInner(innerCtx context.Context, innerReady chan struct{})
res := m.pathManager.readerAdd(pathReaderAddReq{ res := m.pathManager.readerAdd(pathReaderAddReq{
author: m, author: m,
pathName: m.pathName, pathName: m.pathName,
skipAuth: true,
}) })
if res.err != nil { if res.err != nil {
return res.err return res.err
@ -554,13 +555,30 @@ func (m *hlsMuxer) handleRequest(ctx *gin.Context) {
bytesSent: m.bytesSent, bytesSent: m.bytesSent,
} }
err := m.authenticate(ctx) user, pass, hasCredentials := ctx.Request.BasicAuth()
err := authenticate(
m.externalAuthenticationURL,
nil,
m.pathName,
m.path.safeConf(),
false,
authCredentials{
query: ctx.Request.URL.RawQuery,
ip: net.ParseIP(ctx.ClientIP()),
user: user,
pass: pass,
proto: authProtocolHLS,
},
)
if err != nil { if err != nil {
if terr, ok := err.(pathErrAuthCritical); ok { if !hasCredentials {
m.Log(logger.Info, "authentication error: %s", terr.message) ctx.Header("WWW-Authenticate", `Basic realm="mediamtx"`)
w.WriteHeader(http.StatusUnauthorized)
return
} }
ctx.Header("WWW-Authenticate", `Basic realm="mediamtx"`) m.Log(logger.Info, "authentication error: %s", err)
w.WriteHeader(http.StatusUnauthorized) w.WriteHeader(http.StatusUnauthorized)
return return
} }
@ -575,63 +593,6 @@ func (m *hlsMuxer) handleRequest(ctx *gin.Context) {
m.muxer.Handle(w, ctx.Request) m.muxer.Handle(w, ctx.Request)
} }
func (m *hlsMuxer) authenticate(ctx *gin.Context) error {
pathConf := m.path.safeConf()
pathIPs := pathConf.ReadIPs
pathUser := pathConf.ReadUser
pathPass := pathConf.ReadPass
if m.externalAuthenticationURL != "" {
ip := net.ParseIP(ctx.ClientIP())
user, pass, ok := ctx.Request.BasicAuth()
err := externalAuth(
m.externalAuthenticationURL,
ip.String(),
user,
pass,
m.pathName,
externalAuthProtoHLS,
nil,
false,
ctx.Request.URL.RawQuery)
if err != nil {
if !ok {
return pathErrAuthNotCritical{}
}
return pathErrAuthCritical{
message: fmt.Sprintf("external authentication failed: %s", err),
}
}
}
if pathIPs != nil {
ip := net.ParseIP(ctx.ClientIP())
if !ipEqualOrInRange(ip, pathIPs) {
return pathErrAuthCritical{
message: fmt.Sprintf("IP '%s' not allowed", ip),
}
}
}
if pathUser != "" {
user, pass, ok := ctx.Request.BasicAuth()
if !ok {
return pathErrAuthNotCritical{}
}
if user != string(pathUser) || pass != string(pathPass) {
return pathErrAuthCritical{
message: "invalid credentials",
}
}
}
return nil
}
// processRequest is called by hlsserver.Server (forwarded from ServeHTTP). // processRequest is called by hlsserver.Server (forwarded from ServeHTTP).
func (m *hlsMuxer) processRequest(req *hlsMuxerRequest) { func (m *hlsMuxer) processRequest(req *hlsMuxerRequest) {
select { select {

70
internal/core/path.go

@ -10,7 +10,6 @@ import (
"sync/atomic" "sync/atomic"
"time" "time"
"github.com/bluenviron/gortsplib/v3/pkg/base"
"github.com/bluenviron/gortsplib/v3/pkg/media" "github.com/bluenviron/gortsplib/v3/pkg/media"
"github.com/bluenviron/gortsplib/v3/pkg/url" "github.com/bluenviron/gortsplib/v3/pkg/url"
@ -25,39 +24,22 @@ func newEmptyTimer() *time.Timer {
return t return t
} }
type authenticateFunc func( type pathErrAuth struct {
pathIPs []fmt.Stringer, wrapped error
pathUser conf.Credential,
pathPass conf.Credential,
) error
type pathErrNoOnePublishing struct {
pathName string
}
// Error implements the error interface.
func (e pathErrNoOnePublishing) Error() string {
return fmt.Sprintf("no one is publishing to path '%s'", e.pathName)
}
type pathErrAuthNotCritical struct {
message string
response *base.Response
} }
// Error implements the error interface. // Error implements the error interface.
func (pathErrAuthNotCritical) Error() string { func (e pathErrAuth) Error() string {
return "non-critical authentication error" return "authentication error"
} }
type pathErrAuthCritical struct { type pathErrNoOnePublishing struct {
message string pathName string
response *base.Response
} }
// Error implements the error interface. // Error implements the error interface.
func (pathErrAuthCritical) Error() string { func (e pathErrNoOnePublishing) Error() string {
return "critical authentication error" return fmt.Sprintf("no one is publishing to path '%s'", e.pathName)
} }
type pathParent interface { type pathParent interface {
@ -101,6 +83,17 @@ type pathPublisherRemoveReq struct {
res chan struct{} res chan struct{}
} }
type pathGetPathConfRes struct {
conf *conf.PathConf
err error
}
type pathGetPathConfReq struct {
name string
credentials authCredentials
res chan pathGetPathConfRes
}
type pathDescribeRes struct { type pathDescribeRes struct {
path *path path *path
stream *stream stream *stream
@ -109,10 +102,10 @@ type pathDescribeRes struct {
} }
type pathDescribeReq struct { type pathDescribeReq struct {
pathName string pathName string
url *url.URL url *url.URL
authenticate authenticateFunc credentials authCredentials
res chan pathDescribeRes res chan pathDescribeRes
} }
type pathReaderSetupPlayRes struct { type pathReaderSetupPlayRes struct {
@ -122,10 +115,11 @@ type pathReaderSetupPlayRes struct {
} }
type pathReaderAddReq struct { type pathReaderAddReq struct {
author reader author reader
pathName string pathName string
authenticate authenticateFunc skipAuth bool
res chan pathReaderSetupPlayRes credentials authCredentials
res chan pathReaderSetupPlayRes
} }
type pathPublisherAnnounceRes struct { type pathPublisherAnnounceRes struct {
@ -134,10 +128,10 @@ type pathPublisherAnnounceRes struct {
} }
type pathPublisherAddReq struct { type pathPublisherAddReq struct {
author publisher author publisher
pathName string pathName string
authenticate authenticateFunc credentials authCredentials
res chan pathPublisherAnnounceRes res chan pathPublisherAnnounceRes
} }
type pathPublisherRecordRes struct { type pathPublisherRecordRes struct {

134
internal/core/path_manager.go

@ -39,15 +39,17 @@ type pathManagerParent interface {
} }
type pathManager struct { type pathManager struct {
rtspAddress string externalAuthenticationURL string
readTimeout conf.StringDuration rtspAddress string
writeTimeout conf.StringDuration authMethods conf.AuthMethods
readBufferCount int readTimeout conf.StringDuration
udpMaxPayloadSize int writeTimeout conf.StringDuration
pathConfs map[string]*conf.PathConf readBufferCount int
externalCmdPool *externalcmd.Pool udpMaxPayloadSize int
metrics *metrics pathConfs map[string]*conf.PathConf
parent pathManagerParent externalCmdPool *externalcmd.Pool
metrics *metrics
parent pathManagerParent
ctx context.Context ctx context.Context
ctxCancel func() ctxCancel func()
@ -61,6 +63,7 @@ type pathManager struct {
chPathClose chan *path chPathClose chan *path
chPathSourceReady chan *path chPathSourceReady chan *path
chPathSourceNotReady chan *path chPathSourceNotReady chan *path
chPathGetPathConf chan pathGetPathConfReq
chDescribe chan pathDescribeReq chDescribe chan pathDescribeReq
chReaderAdd chan pathReaderAddReq chReaderAdd chan pathReaderAddReq
chPublisherAdd chan pathPublisherAddReq chPublisherAdd chan pathPublisherAddReq
@ -70,7 +73,9 @@ type pathManager struct {
func newPathManager( func newPathManager(
parentCtx context.Context, parentCtx context.Context,
externalAuthenticationURL string,
rtspAddress string, rtspAddress string,
authMethods conf.AuthMethods,
readTimeout conf.StringDuration, readTimeout conf.StringDuration,
writeTimeout conf.StringDuration, writeTimeout conf.StringDuration,
readBufferCount int, readBufferCount int,
@ -83,28 +88,31 @@ func newPathManager(
ctx, ctxCancel := context.WithCancel(parentCtx) ctx, ctxCancel := context.WithCancel(parentCtx)
pm := &pathManager{ pm := &pathManager{
rtspAddress: rtspAddress, externalAuthenticationURL: externalAuthenticationURL,
readTimeout: readTimeout, rtspAddress: rtspAddress,
writeTimeout: writeTimeout, authMethods: authMethods,
readBufferCount: readBufferCount, readTimeout: readTimeout,
udpMaxPayloadSize: udpMaxPayloadSize, writeTimeout: writeTimeout,
pathConfs: pathConfs, readBufferCount: readBufferCount,
externalCmdPool: externalCmdPool, udpMaxPayloadSize: udpMaxPayloadSize,
metrics: metrics, pathConfs: pathConfs,
parent: parent, externalCmdPool: externalCmdPool,
ctx: ctx, metrics: metrics,
ctxCancel: ctxCancel, parent: parent,
paths: make(map[string]*path), ctx: ctx,
pathsByConf: make(map[string]map[*path]struct{}), ctxCancel: ctxCancel,
chConfReload: make(chan map[string]*conf.PathConf), paths: make(map[string]*path),
chPathClose: make(chan *path), pathsByConf: make(map[string]map[*path]struct{}),
chPathSourceReady: make(chan *path), chConfReload: make(chan map[string]*conf.PathConf),
chPathSourceNotReady: make(chan *path), chPathClose: make(chan *path),
chDescribe: make(chan pathDescribeReq), chPathSourceReady: make(chan *path),
chReaderAdd: make(chan pathReaderAddReq), chPathSourceNotReady: make(chan *path),
chPublisherAdd: make(chan pathPublisherAddReq), chPathGetPathConf: make(chan pathGetPathConfReq),
chHLSServerSet: make(chan pathManagerHLSServer), chDescribe: make(chan pathDescribeReq),
chAPIPathsList: make(chan pathAPIPathsListReq), chReaderAdd: make(chan pathReaderAddReq),
chPublisherAdd: make(chan pathPublisherAddReq),
chHLSServerSet: make(chan pathManagerHLSServer),
chAPIPathsList: make(chan pathAPIPathsListReq),
} }
for pathConfName, pathConf := range pm.pathConfs { for pathConfName, pathConf := range pm.pathConfs {
@ -194,22 +202,32 @@ outer:
pm.hlsServer.pathSourceNotReady(pa) pm.hlsServer.pathSourceNotReady(pa)
} }
case req := <-pm.chPathGetPathConf:
_, pathConf, _, err := pm.getPathConfInternal(req.name)
if err != nil {
req.res <- pathGetPathConfRes{err: err}
continue
}
err = authenticate(pm.externalAuthenticationURL, pm.authMethods, req.name, pathConf, false, req.credentials)
if err != nil {
req.res <- pathGetPathConfRes{err: pathErrAuth{wrapped: err}}
continue
}
req.res <- pathGetPathConfRes{conf: pathConf}
case req := <-pm.chDescribe: case req := <-pm.chDescribe:
pathConfName, pathConf, pathMatches, err := pm.findPathConf(req.pathName) pathConfName, pathConf, pathMatches, err := pm.getPathConfInternal(req.pathName)
if err != nil { if err != nil {
req.res <- pathDescribeRes{err: err} req.res <- pathDescribeRes{err: err}
continue continue
} }
if req.authenticate != nil { err = authenticate(pm.externalAuthenticationURL, pm.authMethods, req.pathName, pathConf, false, req.credentials)
err = req.authenticate( if err != nil {
pathConf.ReadIPs, req.res <- pathDescribeRes{err: pathErrAuth{wrapped: err}}
pathConf.ReadUser, continue
pathConf.ReadPass)
if err != nil {
req.res <- pathDescribeRes{err: err}
continue
}
} }
// create path if it doesn't exist // create path if it doesn't exist
@ -220,19 +238,16 @@ outer:
req.res <- pathDescribeRes{path: pm.paths[req.pathName]} req.res <- pathDescribeRes{path: pm.paths[req.pathName]}
case req := <-pm.chReaderAdd: case req := <-pm.chReaderAdd:
pathConfName, pathConf, pathMatches, err := pm.findPathConf(req.pathName) pathConfName, pathConf, pathMatches, err := pm.getPathConfInternal(req.pathName)
if err != nil { if err != nil {
req.res <- pathReaderSetupPlayRes{err: err} req.res <- pathReaderSetupPlayRes{err: err}
continue continue
} }
if req.authenticate != nil { if !req.skipAuth {
err = req.authenticate( err = authenticate(pm.externalAuthenticationURL, pm.authMethods, req.pathName, pathConf, false, req.credentials)
pathConf.ReadIPs,
pathConf.ReadUser,
pathConf.ReadPass)
if err != nil { if err != nil {
req.res <- pathReaderSetupPlayRes{err: err} req.res <- pathReaderSetupPlayRes{err: pathErrAuth{wrapped: err}}
continue continue
} }
} }
@ -245,18 +260,15 @@ outer:
req.res <- pathReaderSetupPlayRes{path: pm.paths[req.pathName]} req.res <- pathReaderSetupPlayRes{path: pm.paths[req.pathName]}
case req := <-pm.chPublisherAdd: case req := <-pm.chPublisherAdd:
pathConfName, pathConf, pathMatches, err := pm.findPathConf(req.pathName) pathConfName, pathConf, pathMatches, err := pm.getPathConfInternal(req.pathName)
if err != nil { if err != nil {
req.res <- pathPublisherAnnounceRes{err: err} req.res <- pathPublisherAnnounceRes{err: err}
continue continue
} }
err = req.authenticate( err = authenticate(pm.externalAuthenticationURL, pm.authMethods, req.pathName, pathConf, true, req.credentials)
pathConf.PublishIPs,
pathConf.PublishUser,
pathConf.PublishPass)
if err != nil { if err != nil {
req.res <- pathPublisherAnnounceRes{err: err} req.res <- pathPublisherAnnounceRes{err: pathErrAuth{wrapped: err}}
continue continue
} }
@ -330,7 +342,7 @@ func (pm *pathManager) removePath(pa *path) {
delete(pm.paths, pa.name) delete(pm.paths, pa.name)
} }
func (pm *pathManager) findPathConf(name string) (string, *conf.PathConf, []string, error) { func (pm *pathManager) getPathConfInternal(name string) (string, *conf.PathConf, []string, error) {
err := conf.IsValidPathName(name) err := conf.IsValidPathName(name)
if err != nil { if err != nil {
return "", nil, nil, fmt.Errorf("invalid path name: %s (%s)", err, name) return "", nil, nil, fmt.Errorf("invalid path name: %s (%s)", err, name)
@ -389,6 +401,18 @@ func (pm *pathManager) onPathClose(pa *path) {
} }
} }
// getPathConf is called by a reader or publisher.
func (pm *pathManager) getPathConf(req pathGetPathConfReq) pathGetPathConfRes {
req.res = make(chan pathGetPathConfRes)
select {
case pm.chPathGetPathConf <- req:
return <-req.res
case <-pm.ctx.Done():
return pathGetPathConfRes{err: fmt.Errorf("terminated")}
}
}
// describe is called by a reader or publisher. // describe is called by a reader or publisher.
func (pm *pathManager) describe(req pathDescribeReq) pathDescribeRes { func (pm *pathManager) describe(req pathDescribeReq) pathDescribeRes {
req.res = make(chan pathDescribeRes) req.res = make(chan pathDescribeRes)

149
internal/core/rtmp_conn.go

@ -196,20 +196,19 @@ type rtmpConnParent interface {
} }
type rtmpConn struct { type rtmpConn struct {
isTLS bool isTLS bool
externalAuthenticationURL string rtspAddress string
rtspAddress string readTimeout conf.StringDuration
readTimeout conf.StringDuration writeTimeout conf.StringDuration
writeTimeout conf.StringDuration readBufferCount int
readBufferCount int runOnConnect string
runOnConnect string runOnConnectRestart bool
runOnConnectRestart bool wg *sync.WaitGroup
wg *sync.WaitGroup conn *rtmp.Conn
conn *rtmp.Conn nconn net.Conn
nconn net.Conn externalCmdPool *externalcmd.Pool
externalCmdPool *externalcmd.Pool pathManager rtmpConnPathManager
pathManager rtmpConnPathManager parent rtmpConnParent
parent rtmpConnParent
ctx context.Context ctx context.Context
ctxCancel func() ctxCancel func()
@ -222,7 +221,6 @@ type rtmpConn struct {
func newRTMPConn( func newRTMPConn(
parentCtx context.Context, parentCtx context.Context,
isTLS bool, isTLS bool,
externalAuthenticationURL string,
rtspAddress string, rtspAddress string,
readTimeout conf.StringDuration, readTimeout conf.StringDuration,
writeTimeout conf.StringDuration, writeTimeout conf.StringDuration,
@ -238,24 +236,23 @@ func newRTMPConn(
ctx, ctxCancel := context.WithCancel(parentCtx) ctx, ctxCancel := context.WithCancel(parentCtx)
c := &rtmpConn{ c := &rtmpConn{
isTLS: isTLS, isTLS: isTLS,
externalAuthenticationURL: externalAuthenticationURL, rtspAddress: rtspAddress,
rtspAddress: rtspAddress, readTimeout: readTimeout,
readTimeout: readTimeout, writeTimeout: writeTimeout,
writeTimeout: writeTimeout, readBufferCount: readBufferCount,
readBufferCount: readBufferCount, runOnConnect: runOnConnect,
runOnConnect: runOnConnect, runOnConnectRestart: runOnConnectRestart,
runOnConnectRestart: runOnConnectRestart, wg: wg,
wg: wg, conn: rtmp.NewConn(nconn),
conn: rtmp.NewConn(nconn), nconn: nconn,
nconn: nconn, externalCmdPool: externalCmdPool,
externalCmdPool: externalCmdPool, pathManager: pathManager,
pathManager: pathManager, parent: parent,
parent: parent, ctx: ctx,
ctx: ctx, ctxCancel: ctxCancel,
ctxCancel: ctxCancel, uuid: uuid.New(),
uuid: uuid.New(), created: time.Now(),
created: time.Now(),
} }
c.Log(logger.Info, "opened") c.Log(logger.Info, "opened")
@ -344,12 +341,12 @@ func (c *rtmpConn) runInner(ctx context.Context) error {
c.nconn.SetReadDeadline(time.Now().Add(time.Duration(c.readTimeout))) c.nconn.SetReadDeadline(time.Now().Add(time.Duration(c.readTimeout)))
c.nconn.SetWriteDeadline(time.Now().Add(time.Duration(c.writeTimeout))) c.nconn.SetWriteDeadline(time.Now().Add(time.Duration(c.writeTimeout)))
u, isPublishing, err := c.conn.InitializeServer() u, publish, err := c.conn.InitializeServer()
if err != nil { if err != nil {
return err return err
} }
if !isPublishing { if !publish {
return c.runRead(ctx, u) return c.runRead(ctx, u)
} }
return c.runPublish(ctx, u) return c.runPublish(ctx, u)
@ -361,20 +358,21 @@ func (c *rtmpConn) runRead(ctx context.Context, u *url.URL) error {
res := c.pathManager.readerAdd(pathReaderAddReq{ res := c.pathManager.readerAdd(pathReaderAddReq{
author: c, author: c,
pathName: pathName, pathName: pathName,
authenticate: func( credentials: authCredentials{
pathIPs []fmt.Stringer, query: rawQuery,
pathUser conf.Credential, ip: c.ip(),
pathPass conf.Credential, user: query.Get("user"),
) error { pass: query.Get("pass"),
return c.authenticate(pathName, pathIPs, pathUser, pathPass, false, query, rawQuery) proto: authProtocolRTMP,
id: &c.uuid,
}, },
}) })
if res.err != nil { if res.err != nil {
if terr, ok := res.err.(pathErrAuthCritical); ok { if terr, ok := res.err.(pathErrAuth); ok {
// wait some seconds to stop brute force attacks // wait some seconds to stop brute force attacks
<-time.After(rtmpConnPauseAfterAuthError) <-time.After(rtmpConnPauseAfterAuthError)
return errors.New(terr.message) return terr.wrapped
} }
return res.err return res.err
} }
@ -716,20 +714,21 @@ func (c *rtmpConn) runPublish(ctx context.Context, u *url.URL) error {
res := c.pathManager.publisherAdd(pathPublisherAddReq{ res := c.pathManager.publisherAdd(pathPublisherAddReq{
author: c, author: c,
pathName: pathName, pathName: pathName,
authenticate: func( credentials: authCredentials{
pathIPs []fmt.Stringer, query: rawQuery,
pathUser conf.Credential, ip: c.ip(),
pathPass conf.Credential, user: query.Get("user"),
) error { pass: query.Get("pass"),
return c.authenticate(pathName, pathIPs, pathUser, pathPass, true, query, rawQuery) proto: authProtocolRTMP,
id: &c.uuid,
}, },
}) })
if res.err != nil { if res.err != nil {
if terr, ok := res.err.(pathErrAuthCritical); ok { if terr, ok := res.err.(pathErrAuth); ok {
// wait some seconds to stop brute force attacks // wait some seconds to stop brute force attacks
<-time.After(rtmpConnPauseAfterAuthError) <-time.After(rtmpConnPauseAfterAuthError)
return errors.New(terr.message) return terr.wrapped
} }
return res.err return res.err
} }
@ -819,54 +818,6 @@ func (c *rtmpConn) runPublish(ctx context.Context, u *url.URL) error {
} }
} }
func (c *rtmpConn) authenticate(
pathName string,
pathIPs []fmt.Stringer,
pathUser conf.Credential,
pathPass conf.Credential,
isPublishing bool,
query url.Values,
rawQuery string,
) error {
if c.externalAuthenticationURL != "" {
err := externalAuth(
c.externalAuthenticationURL,
c.ip().String(),
query.Get("user"),
query.Get("pass"),
pathName,
externalAuthProtoRTMP,
&c.uuid,
isPublishing,
rawQuery)
if err != nil {
return pathErrAuthCritical{
message: fmt.Sprintf("external authentication failed: %s", err),
}
}
}
if pathIPs != nil {
ip := c.ip()
if !ipEqualOrInRange(ip, pathIPs) {
return pathErrAuthCritical{
message: fmt.Sprintf("IP '%s' not allowed", ip),
}
}
}
if pathUser != "" {
if query.Get("user") != string(pathUser) ||
query.Get("pass") != string(pathPass) {
return pathErrAuthCritical{
message: "invalid credentials",
}
}
}
return nil
}
// apiReaderDescribe implements reader. // apiReaderDescribe implements reader.
func (c *rtmpConn) apiReaderDescribe() interface{} { func (c *rtmpConn) apiReaderDescribe() interface{} {
return struct { return struct {

62
internal/core/rtmp_server.go

@ -48,18 +48,17 @@ type rtmpServerParent interface {
} }
type rtmpServer struct { type rtmpServer struct {
externalAuthenticationURL string readTimeout conf.StringDuration
readTimeout conf.StringDuration writeTimeout conf.StringDuration
writeTimeout conf.StringDuration readBufferCount int
readBufferCount int isTLS bool
isTLS bool rtspAddress string
rtspAddress string runOnConnect string
runOnConnect string runOnConnectRestart bool
runOnConnectRestart bool externalCmdPool *externalcmd.Pool
externalCmdPool *externalcmd.Pool metrics *metrics
metrics *metrics pathManager *pathManager
pathManager *pathManager parent rtmpServerParent
parent rtmpServerParent
ctx context.Context ctx context.Context
ctxCancel func() ctxCancel func()
@ -75,7 +74,6 @@ type rtmpServer struct {
func newRTMPServer( func newRTMPServer(
parentCtx context.Context, parentCtx context.Context,
externalAuthenticationURL string,
address string, address string,
readTimeout conf.StringDuration, readTimeout conf.StringDuration,
writeTimeout conf.StringDuration, writeTimeout conf.StringDuration,
@ -111,25 +109,24 @@ func newRTMPServer(
ctx, ctxCancel := context.WithCancel(parentCtx) ctx, ctxCancel := context.WithCancel(parentCtx)
s := &rtmpServer{ s := &rtmpServer{
externalAuthenticationURL: externalAuthenticationURL, readTimeout: readTimeout,
readTimeout: readTimeout, writeTimeout: writeTimeout,
writeTimeout: writeTimeout, readBufferCount: readBufferCount,
readBufferCount: readBufferCount, rtspAddress: rtspAddress,
rtspAddress: rtspAddress, runOnConnect: runOnConnect,
runOnConnect: runOnConnect, runOnConnectRestart: runOnConnectRestart,
runOnConnectRestart: runOnConnectRestart, isTLS: isTLS,
isTLS: isTLS, externalCmdPool: externalCmdPool,
externalCmdPool: externalCmdPool, metrics: metrics,
metrics: metrics, pathManager: pathManager,
pathManager: pathManager, parent: parent,
parent: parent, ctx: ctx,
ctx: ctx, ctxCancel: ctxCancel,
ctxCancel: ctxCancel, ln: ln,
ln: ln, conns: make(map[*rtmpConn]struct{}),
conns: make(map[*rtmpConn]struct{}), chConnClose: make(chan *rtmpConn),
chConnClose: make(chan *rtmpConn), chAPIConnsList: make(chan rtmpServerAPIConnsListReq),
chAPIConnsList: make(chan rtmpServerAPIConnsListReq), chAPIConnsKick: make(chan rtmpServerAPIConnsKickReq),
chAPIConnsKick: make(chan rtmpServerAPIConnsKickReq),
} }
s.Log(logger.Info, "listener opened on %s", address) s.Log(logger.Info, "listener opened on %s", address)
@ -200,7 +197,6 @@ outer:
c := newRTMPConn( c := newRTMPConn(
s.ctx, s.ctx,
s.isTLS, s.isTLS,
s.externalAuthenticationURL,
s.rtspAddress, s.rtspAddress,
s.readTimeout, s.readTimeout,
s.writeTimeout, s.writeTimeout,

235
internal/core/rtsp_conn.go

@ -1,7 +1,6 @@
package core package core
import ( import (
"errors"
"fmt" "fmt"
"net" "net"
"time" "time"
@ -10,7 +9,6 @@ import (
"github.com/bluenviron/gortsplib/v3/pkg/auth" "github.com/bluenviron/gortsplib/v3/pkg/auth"
"github.com/bluenviron/gortsplib/v3/pkg/base" "github.com/bluenviron/gortsplib/v3/pkg/base"
"github.com/bluenviron/gortsplib/v3/pkg/headers" "github.com/bluenviron/gortsplib/v3/pkg/headers"
"github.com/bluenviron/gortsplib/v3/pkg/url"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/aler9/mediamtx/internal/conf" "github.com/aler9/mediamtx/internal/conf"
@ -27,28 +25,24 @@ type rtspConnParent interface {
} }
type rtspConn struct { type rtspConn struct {
externalAuthenticationURL string rtspAddress string
rtspAddress string authMethods []headers.AuthMethod
authMethods []headers.AuthMethod readTimeout conf.StringDuration
readTimeout conf.StringDuration runOnConnect string
runOnConnect string runOnConnectRestart bool
runOnConnectRestart bool externalCmdPool *externalcmd.Pool
externalCmdPool *externalcmd.Pool pathManager *pathManager
pathManager *pathManager conn *gortsplib.ServerConn
conn *gortsplib.ServerConn parent rtspConnParent
parent rtspConnParent
uuid uuid.UUID
uuid uuid.UUID created time.Time
created time.Time onConnectCmd *externalcmd.Cmd
onConnectCmd *externalcmd.Cmd authNonce string
authUser string authFailures int
authPass string
authValidator *auth.Validator
authFailures int
} }
func newRTSPConn( func newRTSPConn(
externalAuthenticationURL string,
rtspAddress string, rtspAddress string,
authMethods []headers.AuthMethod, authMethods []headers.AuthMethod,
readTimeout conf.StringDuration, readTimeout conf.StringDuration,
@ -60,18 +54,17 @@ func newRTSPConn(
parent rtspConnParent, parent rtspConnParent,
) *rtspConn { ) *rtspConn {
c := &rtspConn{ c := &rtspConn{
externalAuthenticationURL: externalAuthenticationURL, rtspAddress: rtspAddress,
rtspAddress: rtspAddress, authMethods: authMethods,
authMethods: authMethods, readTimeout: readTimeout,
readTimeout: readTimeout, runOnConnect: runOnConnect,
runOnConnect: runOnConnect, runOnConnectRestart: runOnConnectRestart,
runOnConnectRestart: runOnConnectRestart, externalCmdPool: externalCmdPool,
externalCmdPool: externalCmdPool, pathManager: pathManager,
pathManager: pathManager, conn: conn,
conn: conn, parent: parent,
parent: parent, uuid: uuid.New(),
uuid: uuid.New(), created: time.Now(),
created: time.Now(),
} }
c.Log(logger.Info, "opened") c.Log(logger.Info, "opened")
@ -112,127 +105,6 @@ func (c *rtspConn) ip() net.IP {
return c.conn.NetConn().RemoteAddr().(*net.TCPAddr).IP return c.conn.NetConn().RemoteAddr().(*net.TCPAddr).IP
} }
func (c *rtspConn) authenticate(
path string,
query string,
pathIPs []fmt.Stringer,
pathUser conf.Credential,
pathPass conf.Credential,
isPublishing bool,
req *base.Request,
baseURL *url.URL,
) error {
if c.externalAuthenticationURL != "" {
username := ""
password := ""
var auth headers.Authorization
err := auth.Unmarshal(req.Header["Authorization"])
if err == nil && auth.Method == headers.AuthBasic {
username = auth.BasicUser
password = auth.BasicPass
}
err = externalAuth(
c.externalAuthenticationURL,
c.ip().String(),
username,
password,
path,
externalAuthProtoRTSP,
&c.uuid,
isPublishing,
query)
if err != nil {
c.authFailures++
// VLC with login prompt sends 4 requests:
// 1) without credentials
// 2) with password but without username
// 3) without credentials
// 4) with password and username
// therefore we must allow up to 3 failures
if c.authFailures > 3 {
return pathErrAuthCritical{
message: "unauthorized: " + err.Error(),
response: &base.Response{
StatusCode: base.StatusUnauthorized,
},
}
}
v := "IPCAM"
return pathErrAuthNotCritical{
message: "unauthorized: " + err.Error(),
response: &base.Response{
StatusCode: base.StatusUnauthorized,
Header: base.Header{
"WWW-Authenticate": headers.Authenticate{
Method: headers.AuthBasic,
Realm: &v,
}.Marshal(),
},
},
}
}
}
if pathIPs != nil {
ip := c.ip()
if !ipEqualOrInRange(ip, pathIPs) {
return pathErrAuthCritical{
message: fmt.Sprintf("IP '%s' not allowed", ip),
response: &base.Response{
StatusCode: base.StatusUnauthorized,
},
}
}
}
if pathUser != "" {
// reset authValidator every time the credentials change
if c.authValidator == nil || c.authUser != string(pathUser) || c.authPass != string(pathPass) {
c.authUser = string(pathUser)
c.authPass = string(pathPass)
c.authValidator = auth.NewValidator(string(pathUser), string(pathPass), c.authMethods)
}
err := c.authValidator.ValidateRequest(req, baseURL)
if err != nil {
c.authFailures++
// VLC with login prompt sends 4 requests:
// 1) without credentials
// 2) with password but without username
// 3) without credentials
// 4) with password and username
// therefore we must allow up to 3 failures
if c.authFailures > 3 {
return pathErrAuthCritical{
message: "unauthorized: " + err.Error(),
response: &base.Response{
StatusCode: base.StatusUnauthorized,
},
}
}
return pathErrAuthNotCritical{
response: &base.Response{
StatusCode: base.StatusUnauthorized,
Header: base.Header{
"WWW-Authenticate": c.authValidator.Header(),
},
},
}
}
// login successful, reset authFailures
c.authFailures = 0
}
return nil
}
// onClose is called by rtspServer. // onClose is called by rtspServer.
func (c *rtspConn) onClose(err error) { func (c *rtspConn) onClose(err error) {
c.Log(logger.Info, "closed (%v)", err) c.Log(logger.Info, "closed (%v)", err)
@ -263,29 +135,28 @@ func (c *rtspConn) onDescribe(ctx *gortsplib.ServerHandlerOnDescribeCtx,
} }
ctx.Path = ctx.Path[1:] ctx.Path = ctx.Path[1:]
if c.authNonce == "" {
c.authNonce = auth.GenerateNonce()
}
res := c.pathManager.describe(pathDescribeReq{ res := c.pathManager.describe(pathDescribeReq{
pathName: ctx.Path, pathName: ctx.Path,
url: ctx.Request.URL, url: ctx.Request.URL,
authenticate: func( credentials: authCredentials{
pathIPs []fmt.Stringer, query: ctx.Query,
pathUser conf.Credential, ip: c.ip(),
pathPass conf.Credential, proto: authProtocolRTSP,
) error { id: &c.uuid,
return c.authenticate(ctx.Path, ctx.Query, pathIPs, pathUser, pathPass, false, ctx.Request, nil) rtspRequest: ctx.Request,
rtspNonce: c.authNonce,
}, },
}) })
if res.err != nil { if res.err != nil {
switch terr := res.err.(type) { switch terr := res.err.(type) {
case pathErrAuthNotCritical: case pathErrAuth:
c.Log(logger.Debug, "non-critical authentication error: %s", terr.message) res, err := c.handleAuthError(terr.wrapped)
return terr.response, nil, nil return res, nil, err
case pathErrAuthCritical:
// wait some seconds to stop brute force attacks
<-time.After(rtspConnPauseAfterAuthError)
return terr.response, nil, errors.New(terr.message)
case pathErrNoOnePublishing: case pathErrNoOnePublishing:
return &base.Response{ return &base.Response{
@ -312,3 +183,29 @@ func (c *rtspConn) onDescribe(ctx *gortsplib.ServerHandlerOnDescribeCtx,
StatusCode: base.StatusOK, StatusCode: base.StatusOK,
}, res.stream.rtspStream, nil }, res.stream.rtspStream, nil
} }
func (c *rtspConn) handleAuthError(authErr error) (*base.Response, error) {
c.authFailures++
// VLC with login prompt sends 4 requests:
// 1) without credentials
// 2) with password but without username
// 3) without credentials
// 4) with password and username
// therefore we must allow up to 3 failures
if c.authFailures <= 3 {
return &base.Response{
StatusCode: base.StatusUnauthorized,
Header: base.Header{
"WWW-Authenticate": auth.GenerateWWWAuthenticate(c.authMethods, "IPCAM", c.authNonce),
},
}, nil
}
// wait some seconds to stop brute force attacks
<-time.After(rtspConnPauseAfterAuthError)
return &base.Response{
StatusCode: base.StatusUnauthorized,
}, authErr
}

56
internal/core/rtsp_server.go

@ -76,18 +76,17 @@ func printAddresses(srv *gortsplib.Server) string {
} }
type rtspServer struct { type rtspServer struct {
externalAuthenticationURL string authMethods []headers.AuthMethod
authMethods []headers.AuthMethod readTimeout conf.StringDuration
readTimeout conf.StringDuration isTLS bool
isTLS bool rtspAddress string
rtspAddress string protocols map[conf.Protocol]struct{}
protocols map[conf.Protocol]struct{} runOnConnect string
runOnConnect string runOnConnectRestart bool
runOnConnectRestart bool externalCmdPool *externalcmd.Pool
externalCmdPool *externalcmd.Pool metrics *metrics
metrics *metrics pathManager *pathManager
pathManager *pathManager parent rtspServerParent
parent rtspServerParent
ctx context.Context ctx context.Context
ctxCancel func() ctxCancel func()
@ -100,7 +99,6 @@ type rtspServer struct {
func newRTSPServer( func newRTSPServer(
parentCtx context.Context, parentCtx context.Context,
externalAuthenticationURL string,
address string, address string,
authMethods []headers.AuthMethod, authMethods []headers.AuthMethod,
readTimeout conf.StringDuration, readTimeout conf.StringDuration,
@ -128,22 +126,21 @@ func newRTSPServer(
ctx, ctxCancel := context.WithCancel(parentCtx) ctx, ctxCancel := context.WithCancel(parentCtx)
s := &rtspServer{ s := &rtspServer{
externalAuthenticationURL: externalAuthenticationURL, authMethods: authMethods,
authMethods: authMethods, readTimeout: readTimeout,
readTimeout: readTimeout, isTLS: isTLS,
isTLS: isTLS, rtspAddress: rtspAddress,
rtspAddress: rtspAddress, protocols: protocols,
protocols: protocols, runOnConnect: runOnConnect,
runOnConnect: runOnConnect, runOnConnectRestart: runOnConnectRestart,
runOnConnectRestart: runOnConnectRestart, externalCmdPool: externalCmdPool,
externalCmdPool: externalCmdPool, metrics: metrics,
metrics: metrics, pathManager: pathManager,
pathManager: pathManager, parent: parent,
parent: parent, ctx: ctx,
ctx: ctx, ctxCancel: ctxCancel,
ctxCancel: ctxCancel, conns: make(map[*gortsplib.ServerConn]*rtspConn),
conns: make(map[*gortsplib.ServerConn]*rtspConn), sessions: make(map[*gortsplib.ServerSession]*rtspSession),
sessions: make(map[*gortsplib.ServerSession]*rtspSession),
} }
s.srv = &gortsplib.Server{ s.srv = &gortsplib.Server{
@ -246,7 +243,6 @@ outer:
// OnConnOpen implements gortsplib.ServerHandlerOnConnOpen. // OnConnOpen implements gortsplib.ServerHandlerOnConnOpen.
func (s *rtspServer) OnConnOpen(ctx *gortsplib.ServerHandlerOnConnOpenCtx) { func (s *rtspServer) OnConnOpen(ctx *gortsplib.ServerHandlerOnConnOpenCtx) {
c := newRTSPConn( c := newRTSPConn(
s.externalAuthenticationURL,
s.rtspAddress, s.rtspAddress,
s.authMethods, s.authMethods,
s.readTimeout, s.readTimeout,

15
internal/core/rtsp_server_test.go

@ -115,13 +115,14 @@ func TestRTSPServerAuth(t *testing.T) {
} }
t.Run("hashed", func(t *testing.T) { t.Run("hashed", func(t *testing.T) {
p, ok := newInstance("rtmpDisable: yes\n" + p, ok := newInstance(
"hlsDisable: yes\n" + "rtmpDisable: yes\n" +
"webrtcDisable: yes\n" + "hlsDisable: yes\n" +
"paths:\n" + "webrtcDisable: yes\n" +
" all:\n" + "paths:\n" +
" publishUser: sha256:rl3rgi4NcZkpAEcacZnQ2VuOfJ0FxAqCRaKB/SwdZoQ=\n" + " all:\n" +
" publishPass: sha256:E9JJ8stBJ7QM+nV4ZoUCeHk/gU3tPFh/5YieiJp6n2w=\n") " publishUser: sha256:rl3rgi4NcZkpAEcacZnQ2VuOfJ0FxAqCRaKB/SwdZoQ=\n" +
" publishPass: sha256:E9JJ8stBJ7QM+nV4ZoUCeHk/gU3tPFh/5YieiJp6n2w=\n")
require.Equal(t, true, ok) require.Equal(t, true, ok)
defer p.Close() defer p.Close()

91
internal/core/rtsp_session.go

@ -2,13 +2,13 @@ package core
import ( import (
"encoding/hex" "encoding/hex"
"errors"
"fmt" "fmt"
"net" "net"
"sync" "sync"
"time" "time"
"github.com/bluenviron/gortsplib/v3" "github.com/bluenviron/gortsplib/v3"
"github.com/bluenviron/gortsplib/v3/pkg/auth"
"github.com/bluenviron/gortsplib/v3/pkg/base" "github.com/bluenviron/gortsplib/v3/pkg/base"
"github.com/bluenviron/gortsplib/v3/pkg/formats" "github.com/bluenviron/gortsplib/v3/pkg/formats"
"github.com/bluenviron/gortsplib/v3/pkg/media" "github.com/bluenviron/gortsplib/v3/pkg/media"
@ -22,10 +22,6 @@ import (
"github.com/aler9/mediamtx/internal/logger" "github.com/aler9/mediamtx/internal/logger"
) )
const (
pauseAfterAuthError = 2 * time.Second
)
type rtspWriteFunc func(*rtp.Packet) type rtspWriteFunc func(*rtp.Packet)
func getRTSPWriteFunc(medi *media.Media, forma formats.Format, stream *stream) rtspWriteFunc { func getRTSPWriteFunc(medi *media.Media, forma formats.Format, stream *stream) rtspWriteFunc {
@ -202,29 +198,28 @@ func (s *rtspSession) onAnnounce(c *rtspConn, ctx *gortsplib.ServerHandlerOnAnno
} }
ctx.Path = ctx.Path[1:] ctx.Path = ctx.Path[1:]
if c.authNonce == "" {
c.authNonce = auth.GenerateNonce()
}
res := s.pathManager.publisherAdd(pathPublisherAddReq{ res := s.pathManager.publisherAdd(pathPublisherAddReq{
author: s, author: s,
pathName: ctx.Path, pathName: ctx.Path,
authenticate: func( credentials: authCredentials{
pathIPs []fmt.Stringer, query: ctx.Query,
pathUser conf.Credential, ip: c.ip(),
pathPass conf.Credential, proto: authProtocolRTSP,
) error { id: &c.uuid,
return c.authenticate(ctx.Path, ctx.Query, pathIPs, pathUser, pathPass, true, ctx.Request, nil) rtspRequest: ctx.Request,
rtspBaseURL: nil,
rtspNonce: c.authNonce,
}, },
}) })
if res.err != nil { if res.err != nil {
switch terr := res.err.(type) { switch terr := res.err.(type) {
case pathErrAuthNotCritical: case pathErrAuth:
s.Log(logger.Debug, "non-critical authentication error: %s", terr.message) return c.handleAuthError(terr.wrapped)
return terr.response, nil
case pathErrAuthCritical:
// wait some seconds to stop brute force attacks
<-time.After(pauseAfterAuthError)
return terr.response, errors.New(terr.message)
default: default:
return &base.Response{ return &base.Response{
@ -268,42 +263,42 @@ func (s *rtspSession) onSetup(c *rtspConn, ctx *gortsplib.ServerHandlerOnSetupCt
switch s.session.State() { switch s.session.State() {
case gortsplib.ServerSessionStateInitial, gortsplib.ServerSessionStatePrePlay: // play case gortsplib.ServerSessionStateInitial, gortsplib.ServerSessionStatePrePlay: // play
baseURL := &url.URL{
Scheme: ctx.Request.URL.Scheme,
Host: ctx.Request.URL.Host,
Path: ctx.Path,
RawQuery: ctx.Query,
}
if ctx.Query != "" {
baseURL.RawQuery += "/"
} else {
baseURL.Path += "/"
}
if c.authNonce == "" {
c.authNonce = auth.GenerateNonce()
}
res := s.pathManager.readerAdd(pathReaderAddReq{ res := s.pathManager.readerAdd(pathReaderAddReq{
author: s, author: s,
pathName: ctx.Path, pathName: ctx.Path,
authenticate: func( credentials: authCredentials{
pathIPs []fmt.Stringer, query: ctx.Query,
pathUser conf.Credential, ip: c.ip(),
pathPass conf.Credential, proto: authProtocolRTSP,
) error { id: &c.uuid,
baseURL := &url.URL{ rtspRequest: ctx.Request,
Scheme: ctx.Request.URL.Scheme, rtspBaseURL: baseURL,
Host: ctx.Request.URL.Host, rtspNonce: c.authNonce,
Path: ctx.Path,
RawQuery: ctx.Query,
}
if ctx.Query != "" {
baseURL.RawQuery += "/"
} else {
baseURL.Path += "/"
}
return c.authenticate(ctx.Path, ctx.Query, pathIPs, pathUser, pathPass, false, ctx.Request, baseURL)
}, },
}) })
if res.err != nil { if res.err != nil {
switch terr := res.err.(type) { switch terr := res.err.(type) {
case pathErrAuthNotCritical: case pathErrAuth:
s.Log(logger.Debug, "non-critical authentication error: %s", terr.message) res, err := c.handleAuthError(terr.wrapped)
return terr.response, nil, nil return res, nil, err
case pathErrAuthCritical:
// wait some seconds to stop brute force attacks
<-time.After(pauseAfterAuthError)
return terr.response, nil, errors.New(terr.message)
case pathErrNoOnePublishing: case pathErrNoOnePublishing:
return &base.Response{ return &base.Response{

9
internal/core/webrtc_conn.go

@ -26,7 +26,6 @@ import (
"github.com/pion/interceptor" "github.com/pion/interceptor"
"github.com/pion/webrtc/v3" "github.com/pion/webrtc/v3"
"github.com/aler9/mediamtx/internal/conf"
"github.com/aler9/mediamtx/internal/formatprocessor" "github.com/aler9/mediamtx/internal/formatprocessor"
"github.com/aler9/mediamtx/internal/logger" "github.com/aler9/mediamtx/internal/logger"
"github.com/aler9/mediamtx/internal/websocket" "github.com/aler9/mediamtx/internal/websocket"
@ -302,13 +301,7 @@ func (c *webRTCConn) runInner(ctx context.Context) error {
res := c.pathManager.readerAdd(pathReaderAddReq{ res := c.pathManager.readerAdd(pathReaderAddReq{
author: c, author: c,
pathName: c.pathName, pathName: c.pathName,
authenticate: func( skipAuth: true,
pathIPs []fmt.Stringer,
pathUser conf.Credential,
pathPass conf.Credential,
) error {
return nil
},
}) })
if res.err != nil { if res.err != nil {
return res.err return res.err

161
internal/core/webrtc_server.go

@ -68,14 +68,13 @@ type webRTCServerParent interface {
} }
type webRTCServer struct { type webRTCServer struct {
externalAuthenticationURL string allowOrigin string
allowOrigin string trustedProxies conf.IPsOrCIDRs
trustedProxies conf.IPsOrCIDRs iceServers []string
iceServers []string readBufferCount int
readBufferCount int pathManager *pathManager
pathManager *pathManager metrics *metrics
metrics *metrics parent webRTCServerParent
parent webRTCServerParent
ctx context.Context ctx context.Context
ctxCancel func() ctxCancel func()
@ -101,7 +100,6 @@ type webRTCServer struct {
func newWebRTCServer( func newWebRTCServer(
parentCtx context.Context, parentCtx context.Context,
externalAuthenticationURL string,
address string, address string,
encryption bool, encryption bool,
serverKey string, serverKey string,
@ -159,28 +157,27 @@ func newWebRTCServer(
ctx, ctxCancel := context.WithCancel(parentCtx) ctx, ctxCancel := context.WithCancel(parentCtx)
s := &webRTCServer{ s := &webRTCServer{
externalAuthenticationURL: externalAuthenticationURL, allowOrigin: allowOrigin,
allowOrigin: allowOrigin, trustedProxies: trustedProxies,
trustedProxies: trustedProxies, iceServers: iceServers,
iceServers: iceServers, readBufferCount: readBufferCount,
readBufferCount: readBufferCount, pathManager: pathManager,
pathManager: pathManager, metrics: metrics,
metrics: metrics, parent: parent,
parent: parent, ctx: ctx,
ctx: ctx, ctxCancel: ctxCancel,
ctxCancel: ctxCancel, ln: ln,
ln: ln, udpMuxLn: udpMuxLn,
udpMuxLn: udpMuxLn, tcpMuxLn: tcpMuxLn,
tcpMuxLn: tcpMuxLn, iceUDPMux: iceUDPMux,
iceUDPMux: iceUDPMux, iceTCPMux: iceTCPMux,
iceTCPMux: iceTCPMux, iceHostNAT1To1IPs: iceHostNAT1To1IPs,
iceHostNAT1To1IPs: iceHostNAT1To1IPs, conns: make(map[*webRTCConn]struct{}),
conns: make(map[*webRTCConn]struct{}), connNew: make(chan webRTCConnNewReq),
connNew: make(chan webRTCConnNewReq), chConnClose: make(chan *webRTCConn),
chConnClose: make(chan *webRTCConn), chAPIConnsList: make(chan webRTCServerAPIConnsListReq),
chAPIConnsList: make(chan webRTCServerAPIConnsListReq), chAPIConnsKick: make(chan webRTCServerAPIConnsKickReq),
chAPIConnsKick: make(chan webRTCServerAPIConnsKickReq), done: make(chan struct{}),
done: make(chan struct{}),
} }
s.requestPool = newHTTPRequestPool() s.requestPool = newHTTPRequestPool()
@ -357,26 +354,32 @@ func (s *webRTCServer) onRequest(ctx *gin.Context) {
} }
dir = strings.TrimSuffix(dir, "/") dir = strings.TrimSuffix(dir, "/")
user, pass, hasCredentials := ctx.Request.BasicAuth()
res := s.pathManager.describe(pathDescribeReq{
pathName: dir, res := s.pathManager.getPathConf(pathGetPathConfReq{
name: dir,
credentials: authCredentials{
query: ctx.Request.URL.RawQuery,
ip: net.ParseIP(ctx.ClientIP()),
user: user,
pass: pass,
proto: authProtocolWebRTC,
},
}) })
if res.err != nil { if res.err != nil {
ctx.Writer.WriteHeader(http.StatusNotFound) if terr, ok := res.err.(pathErrAuth); ok {
return if !hasCredentials {
} ctx.Header("WWW-Authenticate", `Basic realm="mediamtx"`)
ctx.Writer.WriteHeader(http.StatusUnauthorized)
return
}
err := s.authenticate(res.path, ctx) s.Log(logger.Info, "authentication error: %v", terr.wrapped)
if err != nil {
if terr, ok := err.(pathErrAuthCritical); ok {
s.Log(logger.Info, "authentication error: %s", terr.message)
ctx.Writer.Header().Set("WWW-Authenticate", `Basic realm="mediamtx"`)
ctx.Writer.WriteHeader(http.StatusUnauthorized) ctx.Writer.WriteHeader(http.StatusUnauthorized)
return return
} }
ctx.Writer.Header().Set("WWW-Authenticate", `Basic realm="mediamtx"`) ctx.Writer.WriteHeader(http.StatusNotFound)
ctx.Writer.WriteHeader(http.StatusUnauthorized)
return return
} }
@ -394,7 +397,10 @@ func (s *webRTCServer) onRequest(ctx *gin.Context) {
} }
defer wsconn.Close() defer wsconn.Close()
c := s.newConn(dir, wsconn) c := s.newConn(webRTCConnNewReq{
pathName: dir,
wsconn: wsconn,
})
if c == nil { if c == nil {
return return
} }
@ -403,12 +409,8 @@ func (s *webRTCServer) onRequest(ctx *gin.Context) {
} }
} }
func (s *webRTCServer) newConn(dir string, wsconn *websocket.ServerConn) *webRTCConn { func (s *webRTCServer) newConn(req webRTCConnNewReq) *webRTCConn {
req := webRTCConnNewReq{ req.res = make(chan *webRTCConn)
pathName: dir,
wsconn: wsconn,
res: make(chan *webRTCConn),
}
select { select {
case s.connNew <- req: case s.connNew <- req:
@ -418,63 +420,6 @@ func (s *webRTCServer) newConn(dir string, wsconn *websocket.ServerConn) *webRTC
} }
} }
func (s *webRTCServer) authenticate(pa *path, ctx *gin.Context) error {
pathConf := pa.safeConf()
pathIPs := pathConf.ReadIPs
pathUser := pathConf.ReadUser
pathPass := pathConf.ReadPass
if s.externalAuthenticationURL != "" {
ip := net.ParseIP(ctx.ClientIP())
user, pass, ok := ctx.Request.BasicAuth()
err := externalAuth(
s.externalAuthenticationURL,
ip.String(),
user,
pass,
pa.name,
externalAuthProtoWebRTC,
nil,
false,
ctx.Request.URL.RawQuery)
if err != nil {
if !ok {
return pathErrAuthNotCritical{}
}
return pathErrAuthCritical{
message: fmt.Sprintf("external authentication failed: %s", err),
}
}
}
if pathIPs != nil {
ip := net.ParseIP(ctx.ClientIP())
if !ipEqualOrInRange(ip, pathIPs) {
return pathErrAuthCritical{
message: fmt.Sprintf("IP '%s' not allowed", ip),
}
}
}
if pathUser != "" {
user, pass, ok := ctx.Request.BasicAuth()
if !ok {
return pathErrAuthNotCritical{}
}
if user != string(pathUser) || pass != string(pathPass) {
return pathErrAuthCritical{
message: "invalid credentials",
}
}
}
return nil
}
// connClose is called by webRTCConn. // connClose is called by webRTCConn.
func (s *webRTCServer) connClose(c *webRTCConn) { func (s *webRTCServer) connClose(c *webRTCConn) {
select { select {

3
internal/websocket/serverconn.go

@ -23,7 +23,8 @@ var upgrader = websocket.Upgrader{
}, },
} }
// ServerConn is a server-side WebSocket connection with automatic, periodic ping / pong. // ServerConn is a server-side WebSocket connection with
// automatic, periodic ping-pong
type ServerConn struct { type ServerConn struct {
wc *websocket.Conn wc *websocket.Conn

5
mediamtx.yml

@ -96,8 +96,9 @@ multicastRTCPPort: 8003
serverKey: server.key serverKey: server.key
# Path to the server certificate. This is needed only when encryption is "strict" or "optional". # Path to the server certificate. This is needed only when encryption is "strict" or "optional".
serverCert: server.crt serverCert: server.crt
# Authentication methods. # Authentication methods. Available are "basic" and "digest".
authMethods: [basic, digest] # "digest" doesn't provide any additional security and is available for compatibility reasons only.
authMethods: [basic]
############################################### ###############################################
# RTMP parameters # RTMP parameters

Loading…
Cancel
Save