Browse Source

support external authentication (#504) (#517)

pull/745/head
aler9 4 years ago
parent
commit
11760fd79f
  1. 22
      README.md
  2. 2
      apidocs/openapi.yaml
  3. 39
      internal/conf/conf.go
  4. 18
      internal/conf/path.go
  5. 29
      internal/core/api.go
  6. 16
      internal/core/core.go
  7. 42
      internal/core/externalauth.go
  8. 130
      internal/core/hls_muxer.go
  9. 52
      internal/core/hls_server.go
  10. 164
      internal/core/hls_server_test.go
  11. 39
      internal/core/path.go
  12. 130
      internal/core/path_manager.go
  13. 112
      internal/core/rtmp_conn.go
  14. 62
      internal/core/rtmp_server.go
  15. 161
      internal/core/rtmp_server_test.go
  16. 1
      internal/core/rtmp_source.go
  17. 172
      internal/core/rtsp_conn.go
  18. 58
      internal/core/rtsp_server.go
  19. 137
      internal/core/rtsp_server_test.go
  20. 22
      internal/core/rtsp_session.go
  21. 14
      rtsp-simple-server.yml

22
README.md

@ -19,7 +19,7 @@ Features: @@ -19,7 +19,7 @@ Features:
* Each stream can have multiple video and audio tracks, encoded with any codec, including H264, H265, VP8, VP9, MPEG2, MP3, AAC, Opus, PCM, JPEG
* Streams are automatically converted from a protocol to another. For instance, it's possible to publish a stream with RTSP and read it with HLS
* Serve multiple streams at once in separate paths
* Authenticate readers and publishers
* Authenticate users; use internal or external authentication
* Query and control the server through an HTTP API
* Read Prometheus-compatible metrics
* Redirect readers to other RTSP servers (load balancing)
@ -220,6 +220,26 @@ paths: @@ -220,6 +220,26 @@ paths:
**WARNING**: enable encryption or use a VPN to ensure that no one is intercepting the credentials.
Credentials can be sent to an external server:
```yml
externalAuthenticationURL: http://myauthserver/auth
```
Each time a user needs to be authenticated, the specified URL will be requested with the POST method and this payload:
```json
{
"ip": "ip",
"user": "user",
"password": "password",
"path": "path",
"action": "read|publish"
}
```
If the URL returns a status code that begins with `20` (i.e. `200`), authentication is successful, otherwise it fails.
### Encrypt the configuration
The configuration file can be entirely encrypted for security purposes.

2
apidocs/openapi.yaml

@ -31,6 +31,8 @@ components: @@ -31,6 +31,8 @@ components:
type: string
readBufferCount:
type: integer
externalAuthenticationURL:
type: string
api:
type: boolean
apiAddress:

39
internal/conf/conf.go

@ -7,6 +7,7 @@ import ( @@ -7,6 +7,7 @@ import (
"io/ioutil"
"os"
"reflect"
"strings"
"time"
"github.com/aler9/gortsplib"
@ -153,20 +154,21 @@ func loadFromFile(fpath string, conf *Conf) (bool, error) { @@ -153,20 +154,21 @@ func loadFromFile(fpath string, conf *Conf) (bool, error) {
// Conf is a configuration.
type Conf struct {
// general
LogLevel LogLevel `json:"logLevel"`
LogDestinations LogDestinations `json:"logDestinations"`
LogFile string `json:"logFile"`
ReadTimeout StringDuration `json:"readTimeout"`
WriteTimeout StringDuration `json:"writeTimeout"`
ReadBufferCount int `json:"readBufferCount"`
API bool `json:"api"`
APIAddress string `json:"apiAddress"`
Metrics bool `json:"metrics"`
MetricsAddress string `json:"metricsAddress"`
PPROF bool `json:"pprof"`
PPROFAddress string `json:"pprofAddress"`
RunOnConnect string `json:"runOnConnect"`
RunOnConnectRestart bool `json:"runOnConnectRestart"`
LogLevel LogLevel `json:"logLevel"`
LogDestinations LogDestinations `json:"logDestinations"`
LogFile string `json:"logFile"`
ReadTimeout StringDuration `json:"readTimeout"`
WriteTimeout StringDuration `json:"writeTimeout"`
ReadBufferCount int `json:"readBufferCount"`
ExternalAuthenticationURL string `json:"externalAuthenticationURL"`
API bool `json:"api"`
APIAddress string `json:"apiAddress"`
Metrics bool `json:"metrics"`
MetricsAddress string `json:"metricsAddress"`
PPROF bool `json:"pprof"`
PPROFAddress string `json:"pprofAddress"`
RunOnConnect string `json:"runOnConnect"`
RunOnConnectRestart bool `json:"runOnConnectRestart"`
// RTSP
RTSPDisable bool `json:"rtspDisable"`
@ -248,6 +250,13 @@ func (conf *Conf) CheckAndFillMissing() error { @@ -248,6 +250,13 @@ func (conf *Conf) CheckAndFillMissing() error {
conf.ReadBufferCount = 512
}
if conf.ExternalAuthenticationURL != "" {
if !strings.HasPrefix(conf.ExternalAuthenticationURL, "http://") &&
!strings.HasPrefix(conf.ExternalAuthenticationURL, "https://") {
return fmt.Errorf("'externalAuthenticationURL' must be a HTTP URL")
}
}
if conf.APIAddress == "" {
conf.APIAddress = "127.0.0.1:9997"
}
@ -356,7 +365,7 @@ func (conf *Conf) CheckAndFillMissing() error { @@ -356,7 +365,7 @@ func (conf *Conf) CheckAndFillMissing() error {
pconf = conf.Paths[name]
}
err := pconf.checkAndFillMissing(name)
err := pconf.checkAndFillMissing(conf, name)
if err != nil {
return err
}

18
internal/conf/path.go

@ -71,7 +71,7 @@ type PathConf struct { @@ -71,7 +71,7 @@ type PathConf struct {
RunOnReadRestart bool `json:"runOnReadRestart"`
}
func (pconf *PathConf) checkAndFillMissing(name string) error {
func (pconf *PathConf) checkAndFillMissing(conf *Conf, name string) error {
if name == "" {
return fmt.Errorf("path name can not be empty")
}
@ -207,16 +207,32 @@ func (pconf *PathConf) checkAndFillMissing(name string) error { @@ -207,16 +207,32 @@ func (pconf *PathConf) checkAndFillMissing(name string) error {
"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" {
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")
}
if len(pconf.PublishIPs) > 0 && conf.ExternalAuthenticationURL != "" {
return fmt.Errorf("'publishIPs' can't be used with 'externalAuthenticationURL'")
}
if (pconf.ReadUser != "" && pconf.ReadPass == "") ||
(pconf.ReadUser == "" && pconf.ReadPass != "") {
return fmt.Errorf("read username and password must be both filled")
}
if pconf.ReadUser != "" && conf.ExternalAuthenticationURL != "" {
return fmt.Errorf("'readUser' can't be used with 'externalAuthenticationURL'")
}
if len(pconf.ReadIPs) > 0 && conf.ExternalAuthenticationURL != "" {
return fmt.Errorf("'readIPs' can't be used with 'externalAuthenticationURL'")
}
if pconf.RunOnInit != "" && pconf.Regexp != nil {
return fmt.Errorf("a path with a regular expression does not support option 'runOnInit'; use another path")
}

29
internal/core/api.go

@ -44,20 +44,21 @@ func cloneStruct(dest interface{}, source interface{}) { @@ -44,20 +44,21 @@ func cloneStruct(dest interface{}, source interface{}) {
func loadConfData(ctx *gin.Context) (interface{}, error) {
var in struct {
// general
LogLevel *conf.LogLevel `json:"logLevel"`
LogDestinations *conf.LogDestinations `json:"logDestinations"`
LogFile *string `json:"logFile"`
ReadTimeout *conf.StringDuration `json:"readTimeout"`
WriteTimeout *conf.StringDuration `json:"writeTimeout"`
ReadBufferCount *int `json:"readBufferCount"`
API *bool `json:"api"`
APIAddress *string `json:"apiAddress"`
Metrics *bool `json:"metrics"`
MetricsAddress *string `json:"metricsAddress"`
PPROF *bool `json:"pprof"`
PPROFAddress *string `json:"pprofAddress"`
RunOnConnect *string `json:"runOnConnect"`
RunOnConnectRestart *bool `json:"runOnConnectRestart"`
LogLevel *conf.LogLevel `json:"logLevel"`
LogDestinations *conf.LogDestinations `json:"logDestinations"`
LogFile *string `json:"logFile"`
ReadTimeout *conf.StringDuration `json:"readTimeout"`
WriteTimeout *conf.StringDuration `json:"writeTimeout"`
ReadBufferCount *int `json:"readBufferCount"`
ExternalAuthenticationURL *string `json:"externalAuthenticationURL"`
API *bool `json:"api"`
APIAddress *string `json:"apiAddress"`
Metrics *bool `json:"metrics"`
MetricsAddress *string `json:"metricsAddress"`
PPROF *bool `json:"pprof"`
PPROFAddress *string `json:"pprofAddress"`
RunOnConnect *string `json:"runOnConnect"`
RunOnConnectRestart *bool `json:"runOnConnectRestart"`
// RTSP
RTSPDisable *bool `json:"rtspDisable"`

16
internal/core/core.go

@ -220,13 +220,13 @@ func (p *Core) createResources(initial bool) error { @@ -220,13 +220,13 @@ func (p *Core) createResources(initial bool) error {
if p.pathManager == nil {
p.pathManager = newPathManager(
p.ctx,
p.externalCmdPool,
p.conf.RTSPAddress,
p.conf.ReadTimeout,
p.conf.WriteTimeout,
p.conf.ReadBufferCount,
p.conf.ReadBufferSize,
p.conf.Paths,
p.externalCmdPool,
p.metrics,
p)
}
@ -239,7 +239,7 @@ func (p *Core) createResources(initial bool) error { @@ -239,7 +239,7 @@ func (p *Core) createResources(initial bool) error {
_, useMulticast := p.conf.Protocols[conf.Protocol(gortsplib.TransportUDPMulticast)]
p.rtspServer, err = newRTSPServer(
p.ctx,
p.externalCmdPool,
p.conf.ExternalAuthenticationURL,
p.conf.RTSPAddress,
p.conf.AuthMethods,
p.conf.ReadTimeout,
@ -260,6 +260,7 @@ func (p *Core) createResources(initial bool) error { @@ -260,6 +260,7 @@ func (p *Core) createResources(initial bool) error {
p.conf.Protocols,
p.conf.RunOnConnect,
p.conf.RunOnConnectRestart,
p.externalCmdPool,
p.metrics,
p.pathManager,
p)
@ -275,7 +276,7 @@ func (p *Core) createResources(initial bool) error { @@ -275,7 +276,7 @@ func (p *Core) createResources(initial bool) error {
if p.rtspsServer == nil {
p.rtspsServer, err = newRTSPServer(
p.ctx,
p.externalCmdPool,
p.conf.ExternalAuthenticationURL,
p.conf.RTSPSAddress,
p.conf.AuthMethods,
p.conf.ReadTimeout,
@ -296,6 +297,7 @@ func (p *Core) createResources(initial bool) error { @@ -296,6 +297,7 @@ func (p *Core) createResources(initial bool) error {
p.conf.Protocols,
p.conf.RunOnConnect,
p.conf.RunOnConnectRestart,
p.externalCmdPool,
p.metrics,
p.pathManager,
p)
@ -309,7 +311,7 @@ func (p *Core) createResources(initial bool) error { @@ -309,7 +311,7 @@ func (p *Core) createResources(initial bool) error {
if p.rtmpServer == nil {
p.rtmpServer, err = newRTMPServer(
p.ctx,
p.externalCmdPool,
p.conf.ExternalAuthenticationURL,
p.conf.RTMPAddress,
p.conf.ReadTimeout,
p.conf.WriteTimeout,
@ -317,6 +319,7 @@ func (p *Core) createResources(initial bool) error { @@ -317,6 +319,7 @@ func (p *Core) createResources(initial bool) error {
p.conf.RTSPAddress,
p.conf.RunOnConnect,
p.conf.RunOnConnectRestart,
p.externalCmdPool,
p.metrics,
p.pathManager,
p)
@ -331,6 +334,7 @@ func (p *Core) createResources(initial bool) error { @@ -331,6 +334,7 @@ func (p *Core) createResources(initial bool) error {
p.hlsServer, err = newHLSServer(
p.ctx,
p.conf.HLSAddress,
p.conf.ExternalAuthenticationURL,
p.conf.HLSAlwaysRemux,
p.conf.HLSSegmentCount,
p.conf.HLSSegmentDuration,
@ -411,6 +415,7 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) { @@ -411,6 +415,7 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) {
if newConf == nil ||
newConf.RTSPDisable != p.conf.RTSPDisable ||
newConf.Encryption != p.conf.Encryption ||
newConf.ExternalAuthenticationURL != p.conf.ExternalAuthenticationURL ||
newConf.RTSPAddress != p.conf.RTSPAddress ||
!reflect.DeepEqual(newConf.AuthMethods, p.conf.AuthMethods) ||
newConf.ReadTimeout != p.conf.ReadTimeout ||
@ -435,6 +440,7 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) { @@ -435,6 +440,7 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) {
if newConf == nil ||
newConf.RTSPDisable != p.conf.RTSPDisable ||
newConf.Encryption != p.conf.Encryption ||
newConf.ExternalAuthenticationURL != p.conf.ExternalAuthenticationURL ||
newConf.RTSPSAddress != p.conf.RTSPSAddress ||
!reflect.DeepEqual(newConf.AuthMethods, p.conf.AuthMethods) ||
newConf.ReadTimeout != p.conf.ReadTimeout ||
@ -455,6 +461,7 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) { @@ -455,6 +461,7 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) {
if newConf == nil ||
newConf.RTMPDisable != p.conf.RTMPDisable ||
newConf.RTMPAddress != p.conf.RTMPAddress ||
newConf.ExternalAuthenticationURL != p.conf.ExternalAuthenticationURL ||
newConf.ReadTimeout != p.conf.ReadTimeout ||
newConf.WriteTimeout != p.conf.WriteTimeout ||
newConf.ReadBufferCount != p.conf.ReadBufferCount ||
@ -470,6 +477,7 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) { @@ -470,6 +477,7 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) {
if newConf == nil ||
newConf.HLSDisable != p.conf.HLSDisable ||
newConf.HLSAddress != p.conf.HLSAddress ||
newConf.ExternalAuthenticationURL != p.conf.ExternalAuthenticationURL ||
newConf.HLSAlwaysRemux != p.conf.HLSAlwaysRemux ||
newConf.HLSSegmentCount != p.conf.HLSSegmentCount ||
newConf.HLSSegmentDuration != p.conf.HLSSegmentDuration ||

42
internal/core/externalauth.go

@ -0,0 +1,42 @@ @@ -0,0 +1,42 @@
package core
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
)
func externalAuth(
ur string,
ip string,
user string,
password string,
path string,
action string,
) error {
enc, _ := json.Marshal(struct {
IP string `json:"ip"`
User string `json:"user"`
Password string `json:"password"`
Path string `json:"path"`
Action string `json:"action"`
}{
IP: ip,
User: user,
Password: password,
Path: path,
Action: action,
})
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 {
return fmt.Errorf("bad status code: %d", res.StatusCode)
}
return nil
}

130
internal/core/hls_muxer.go

@ -122,15 +122,16 @@ type hlsMuxerParent interface { @@ -122,15 +122,16 @@ type hlsMuxerParent interface {
}
type hlsMuxer struct {
name string
hlsAlwaysRemux bool
hlsSegmentCount int
hlsSegmentDuration conf.StringDuration
readBufferCount int
wg *sync.WaitGroup
pathName string
pathManager hlsMuxerPathManager
parent hlsMuxerParent
name string
externalAuthenticationURL string
hlsAlwaysRemux bool
hlsSegmentCount int
hlsSegmentDuration conf.StringDuration
readBufferCount int
wg *sync.WaitGroup
pathName string
pathManager hlsMuxerPathManager
parent hlsMuxerParent
ctx context.Context
ctxCancel func()
@ -148,6 +149,7 @@ type hlsMuxer struct { @@ -148,6 +149,7 @@ type hlsMuxer struct {
func newHLSMuxer(
parentCtx context.Context,
name string,
externalAuthenticationURL string,
hlsAlwaysRemux bool,
hlsSegmentCount int,
hlsSegmentDuration conf.StringDuration,
@ -159,17 +161,18 @@ func newHLSMuxer( @@ -159,17 +161,18 @@ func newHLSMuxer(
ctx, ctxCancel := context.WithCancel(parentCtx)
m := &hlsMuxer{
name: name,
hlsAlwaysRemux: hlsAlwaysRemux,
hlsSegmentCount: hlsSegmentCount,
hlsSegmentDuration: hlsSegmentDuration,
readBufferCount: readBufferCount,
wg: wg,
pathName: pathName,
pathManager: pathManager,
parent: parent,
ctx: ctx,
ctxCancel: ctxCancel,
name: name,
externalAuthenticationURL: externalAuthenticationURL,
hlsAlwaysRemux: hlsAlwaysRemux,
hlsSegmentCount: hlsSegmentCount,
hlsSegmentDuration: hlsSegmentDuration,
readBufferCount: readBufferCount,
wg: wg,
pathName: pathName,
pathManager: pathManager,
parent: parent,
ctx: ctx,
ctxCancel: ctxCancel,
lastRequestTime: func() *int64 {
v := time.Now().Unix()
return &v
@ -259,10 +262,9 @@ func (m *hlsMuxer) run() { @@ -259,10 +262,9 @@ func (m *hlsMuxer) run() {
func (m *hlsMuxer) runInner(innerCtx context.Context, innerReady chan struct{}) error {
res := m.pathManager.onReaderSetupPlay(pathReaderSetupPlayReq{
Author: m,
PathName: m.pathName,
IP: nil,
ValidateCredentials: nil,
Author: m,
PathName: m.pathName,
Authenticate: nil,
})
if res.Err != nil {
return res.Err
@ -413,27 +415,21 @@ func (m *hlsMuxer) runInner(innerCtx context.Context, innerReady chan struct{}) @@ -413,27 +415,21 @@ func (m *hlsMuxer) runInner(innerCtx context.Context, innerReady chan struct{})
func (m *hlsMuxer) handleRequest(req hlsMuxerRequest) hlsMuxerResponse {
atomic.StoreInt64(m.lastRequestTime, time.Now().Unix())
conf := m.path.Conf()
if conf.ReadIPs != nil {
tmp, _, _ := net.SplitHostPort(req.Req.RemoteAddr)
ip := net.ParseIP(tmp)
if !ipEqualOrInRange(ip, conf.ReadIPs) {
m.log(logger.Info, "ip '%s' not allowed", ip)
return hlsMuxerResponse{Status: http.StatusUnauthorized}
}
}
if conf.ReadUser != "" {
user, pass, ok := req.Req.BasicAuth()
if !ok || user != string(conf.ReadUser) || pass != string(conf.ReadPass) {
err := m.authenticate(req.Req)
if err != nil {
if terr, ok := err.(pathErrAuthCritical); ok {
m.log(logger.Info, "authentication error: %s", terr.Message)
return hlsMuxerResponse{
Status: http.StatusUnauthorized,
Header: map[string]string{
"WWW-Authenticate": `Basic realm="rtsp-simple-server"`,
},
}
}
return hlsMuxerResponse{
Status: http.StatusUnauthorized,
Header: map[string]string{
"WWW-Authenticate": `Basic realm="rtsp-simple-server"`,
},
}
}
switch {
@ -483,6 +479,58 @@ func (m *hlsMuxer) handleRequest(req hlsMuxerRequest) hlsMuxerResponse { @@ -483,6 +479,58 @@ func (m *hlsMuxer) handleRequest(req hlsMuxerRequest) hlsMuxerResponse {
}
}
func (m *hlsMuxer) authenticate(req *http.Request) error {
pathConf := m.path.Conf()
pathIPs := pathConf.ReadIPs
pathUser := pathConf.ReadUser
pathPass := pathConf.ReadPass
if m.externalAuthenticationURL != "" {
tmp, _, _ := net.SplitHostPort(req.RemoteAddr)
ip := net.ParseIP(tmp)
user, pass, _ := req.BasicAuth()
err := externalAuth(
m.externalAuthenticationURL,
ip.String(),
user,
pass,
m.pathName,
"read")
if err != nil {
return pathErrAuthCritical{
Message: fmt.Sprintf("external authentication failed: %s", err),
}
}
}
if pathIPs != nil {
tmp, _, _ := net.SplitHostPort(req.RemoteAddr)
ip := net.ParseIP(tmp)
if !ipEqualOrInRange(ip, pathIPs) {
return pathErrAuthCritical{
Message: fmt.Sprintf("IP '%s' not allowed", ip),
}
}
}
if pathUser != "" {
user, pass, ok := req.BasicAuth()
if !ok {
return pathErrAuthNotCritical{}
}
if user != string(pathUser) || pass != string(pathPass) {
return pathErrAuthCritical{
Message: "invalid credentials",
}
}
}
return nil
}
// onRequest is called by hlsserver.Server (forwarded from ServeHTTP).
func (m *hlsMuxer) onRequest(req hlsMuxerRequest) {
select {

52
internal/core/hls_server.go

@ -45,14 +45,15 @@ type hlsServerParent interface { @@ -45,14 +45,15 @@ type hlsServerParent interface {
}
type hlsServer struct {
hlsAlwaysRemux bool
hlsSegmentCount int
hlsSegmentDuration conf.StringDuration
hlsAllowOrigin string
readBufferCount int
pathManager *pathManager
metrics *metrics
parent hlsServerParent
externalAuthenticationURL string
hlsAlwaysRemux bool
hlsSegmentCount int
hlsSegmentDuration conf.StringDuration
hlsAllowOrigin string
readBufferCount int
pathManager *pathManager
metrics *metrics
parent hlsServerParent
ctx context.Context
ctxCancel func()
@ -70,6 +71,7 @@ type hlsServer struct { @@ -70,6 +71,7 @@ type hlsServer struct {
func newHLSServer(
parentCtx context.Context,
address string,
externalAuthenticationURL string,
hlsAlwaysRemux bool,
hlsSegmentCount int,
hlsSegmentDuration conf.StringDuration,
@ -87,22 +89,23 @@ func newHLSServer( @@ -87,22 +89,23 @@ func newHLSServer(
ctx, ctxCancel := context.WithCancel(parentCtx)
s := &hlsServer{
hlsAlwaysRemux: hlsAlwaysRemux,
hlsSegmentCount: hlsSegmentCount,
hlsSegmentDuration: hlsSegmentDuration,
hlsAllowOrigin: hlsAllowOrigin,
readBufferCount: readBufferCount,
pathManager: pathManager,
parent: parent,
metrics: metrics,
ctx: ctx,
ctxCancel: ctxCancel,
ln: ln,
muxers: make(map[string]*hlsMuxer),
pathSourceReady: make(chan *path),
request: make(chan hlsMuxerRequest),
muxerClose: make(chan *hlsMuxer),
apiMuxersList: make(chan hlsServerAPIMuxersListReq),
externalAuthenticationURL: externalAuthenticationURL,
hlsAlwaysRemux: hlsAlwaysRemux,
hlsSegmentCount: hlsSegmentCount,
hlsSegmentDuration: hlsSegmentDuration,
hlsAllowOrigin: hlsAllowOrigin,
readBufferCount: readBufferCount,
pathManager: pathManager,
parent: parent,
metrics: metrics,
ctx: ctx,
ctxCancel: ctxCancel,
ln: ln,
muxers: make(map[string]*hlsMuxer),
pathSourceReady: make(chan *path),
request: make(chan hlsMuxerRequest),
muxerClose: make(chan *hlsMuxer),
apiMuxersList: make(chan hlsServerAPIMuxersListReq),
}
s.log(logger.Info, "listener opened on "+address)
@ -268,6 +271,7 @@ func (s *hlsServer) findOrCreateMuxer(pathName string) *hlsMuxer { @@ -268,6 +271,7 @@ func (s *hlsServer) findOrCreateMuxer(pathName string) *hlsMuxer {
r = newHLSMuxer(
s.ctx,
pathName,
s.externalAuthenticationURL,
s.hlsAlwaysRemux,
s.hlsSegmentCount,
s.hlsSegmentDuration,

164
internal/core/hls_server_test.go

@ -1,13 +1,77 @@ @@ -1,13 +1,77 @@
package core
import (
"context"
"encoding/json"
"net"
"net/http"
"testing"
"time"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/require"
)
type testHTTPAuthenticator struct {
action string
s *http.Server
}
func newTestHTTPAuthenticator(action string) (*testHTTPAuthenticator, error) {
ln, err := net.Listen("tcp", "localhost:9120")
if err != nil {
return nil, err
}
ts := &testHTTPAuthenticator{
action: action,
}
router := gin.New()
router.POST("/auth", ts.onAuth)
ts.s = &http.Server{Handler: router}
go ts.s.Serve(ln)
return ts, nil
}
func (ts *testHTTPAuthenticator) close() {
ts.s.Shutdown(context.Background())
}
func (ts *testHTTPAuthenticator) onAuth(ctx *gin.Context) {
var in struct {
IP string `json:"ip"`
User string `json:"user"`
Password string `json:"password"`
Path string `json:"path"`
Action string `json:"action"`
}
err := json.NewDecoder(ctx.Request.Body).Decode(&in)
if err != nil {
ctx.AbortWithStatus(http.StatusBadRequest)
return
}
var user string
if ts.action == "publish" {
user = "testpublisher"
} else {
user = "testreader"
}
if in.IP != "127.0.0.1" ||
in.User != user ||
in.Password != "testpass" ||
in.Path != "teststream" ||
in.Action != ts.action {
ctx.AbortWithStatus(http.StatusBadRequest)
return
}
}
func TestHLSServerNotFound(t *testing.T) {
p, ok := newInstance("")
require.Equal(t, true, ok)
@ -52,36 +116,78 @@ func TestHLSServerRead(t *testing.T) { @@ -52,36 +116,78 @@ func TestHLSServerRead(t *testing.T) {
require.Equal(t, 0, cnt2.wait())
}
func TestHLSServerReadAuth(t *testing.T) {
p, ok := newInstance(
"paths:\n" +
" all:\n" +
" readUser: testuser\n" +
" readPass: testpass\n" +
" readIPs: [127.0.0.0/16]\n")
require.Equal(t, true, ok)
defer p.close()
func TestHLSServerAuth(t *testing.T) {
for _, mode := range []string{
"internal",
"external",
} {
for _, result := range []string{
"success",
"fail",
} {
t.Run(mode+"_"+result, func(t *testing.T) {
var conf string
if mode == "internal" {
conf = "paths:\n" +
" all:\n" +
" readUser: testreader\n" +
" readPass: testpass\n" +
" readIPs: [127.0.0.0/16]\n"
} else {
conf = "externalAuthenticationURL: http://localhost:9120/auth\n" +
"paths:\n" +
" all:\n"
}
cnt1, err := newContainer("ffmpeg", "source", []string{
"-re",
"-stream_loop", "-1",
"-i", "emptyvideo.mkv",
"-c", "copy",
"-f", "rtsp",
"rtsp://localhost:8554/teststream",
})
require.NoError(t, err)
defer cnt1.close()
p, ok := newInstance(conf)
require.Equal(t, true, ok)
defer p.close()
time.Sleep(1 * time.Second)
var a *testHTTPAuthenticator
if mode == "external" {
var err error
a, err = newTestHTTPAuthenticator("publish")
require.NoError(t, err)
}
cnt2, err := newContainer("ffmpeg", "dest", []string{
"-i", "http://testuser:testpass@127.0.0.1:8888/teststream/index.m3u8",
"-vframes", "1",
"-f", "image2",
"-y", "/dev/null",
})
require.NoError(t, err)
defer cnt2.close()
require.Equal(t, 0, cnt2.wait())
cnt1, err := newContainer("ffmpeg", "source", []string{
"-re",
"-stream_loop", "-1",
"-i", "emptyvideo.mkv",
"-c", "copy",
"-f", "rtsp",
"rtsp://testpublisher:testpass@localhost:8554/teststream",
})
require.NoError(t, err)
defer cnt1.close()
time.Sleep(1 * time.Second)
if mode == "external" {
a.close()
var err error
a, err = newTestHTTPAuthenticator("read")
require.NoError(t, err)
defer a.close()
}
var usr string
if result == "success" {
usr = "testreader"
} else {
usr = "testreader2"
}
res, err := http.Get("http://" + usr + ":testpass@127.0.0.1:8888/teststream/index.m3u8")
require.NoError(t, err)
defer res.Body.Close()
if result == "success" {
require.Equal(t, http.StatusOK, res.StatusCode)
} else {
require.Equal(t, http.StatusUnauthorized, res.StatusCode)
}
})
}
}
}

39
internal/core/path.go

@ -23,6 +23,12 @@ func newEmptyTimer() *time.Timer { @@ -23,6 +23,12 @@ func newEmptyTimer() *time.Timer {
return t
}
type authenticateFunc func(
pathIPs []interface{},
pathUser conf.Credential,
pathPass conf.Credential,
) error
type pathErrNoOnePublishing struct {
PathName string
}
@ -120,11 +126,10 @@ type pathDescribeRes struct { @@ -120,11 +126,10 @@ type pathDescribeRes struct {
}
type pathDescribeReq struct {
PathName string
URL *base.URL
IP net.IP
ValidateCredentials func(pathUser conf.Credential, pathPass conf.Credential) error
Res chan pathDescribeRes
PathName string
URL *base.URL
Authenticate authenticateFunc
Res chan pathDescribeRes
}
type pathReaderSetupPlayRes struct {
@ -134,11 +139,10 @@ type pathReaderSetupPlayRes struct { @@ -134,11 +139,10 @@ type pathReaderSetupPlayRes struct {
}
type pathReaderSetupPlayReq struct {
Author reader
PathName string
IP net.IP
ValidateCredentials func(pathUser conf.Credential, pathPass conf.Credential) error
Res chan pathReaderSetupPlayRes
Author reader
PathName string
Authenticate authenticateFunc
Res chan pathReaderSetupPlayRes
}
type pathPublisherAnnounceRes struct {
@ -147,11 +151,10 @@ type pathPublisherAnnounceRes struct { @@ -147,11 +151,10 @@ type pathPublisherAnnounceRes struct {
}
type pathPublisherAnnounceReq struct {
Author publisher
PathName string
IP net.IP
ValidateCredentials func(pathUser conf.Credential, pathPass conf.Credential) error
Res chan pathPublisherAnnounceRes
Author publisher
PathName string
Authenticate authenticateFunc
Res chan pathPublisherAnnounceRes
}
type pathReaderPlayReq struct {
@ -208,7 +211,6 @@ type pathAPIPathsListSubReq struct { @@ -208,7 +211,6 @@ type pathAPIPathsListSubReq struct {
}
type path struct {
externalCmdPool *externalcmd.Pool
rtspAddress string
readTimeout conf.StringDuration
writeTimeout conf.StringDuration
@ -219,6 +221,7 @@ type path struct { @@ -219,6 +221,7 @@ type path struct {
name string
matches []string
wg *sync.WaitGroup
externalCmdPool *externalcmd.Pool
parent pathParent
ctx context.Context
@ -253,7 +256,6 @@ type path struct { @@ -253,7 +256,6 @@ type path struct {
func newPath(
parentCtx context.Context,
externalCmdPool *externalcmd.Pool,
rtspAddress string,
readTimeout conf.StringDuration,
writeTimeout conf.StringDuration,
@ -264,11 +266,11 @@ func newPath( @@ -264,11 +266,11 @@ func newPath(
name string,
matches []string,
wg *sync.WaitGroup,
externalCmdPool *externalcmd.Pool,
parent pathParent) *path {
ctx, ctxCancel := context.WithCancel(parentCtx)
pa := &path{
externalCmdPool: externalCmdPool,
rtspAddress: rtspAddress,
readTimeout: readTimeout,
writeTimeout: writeTimeout,
@ -279,6 +281,7 @@ func newPath( @@ -279,6 +281,7 @@ func newPath(
name: name,
matches: matches,
wg: wg,
externalCmdPool: externalCmdPool,
parent: parent,
ctx: ctx,
ctxCancel: ctxCancel,

130
internal/core/path_manager.go

@ -3,11 +3,8 @@ package core @@ -3,11 +3,8 @@ package core
import (
"context"
"fmt"
"net"
"sync"
"github.com/aler9/gortsplib/pkg/base"
"github.com/aler9/rtsp-simple-server/internal/conf"
"github.com/aler9/rtsp-simple-server/internal/externalcmd"
"github.com/aler9/rtsp-simple-server/internal/logger"
@ -22,13 +19,13 @@ type pathManagerParent interface { @@ -22,13 +19,13 @@ type pathManagerParent interface {
}
type pathManager struct {
externalCmdPool *externalcmd.Pool
rtspAddress string
readTimeout conf.StringDuration
writeTimeout conf.StringDuration
readBufferCount int
readBufferSize int
pathConfs map[string]*conf.PathConf
externalCmdPool *externalcmd.Pool
metrics *metrics
parent pathManagerParent
@ -51,25 +48,25 @@ type pathManager struct { @@ -51,25 +48,25 @@ type pathManager struct {
func newPathManager(
parentCtx context.Context,
externalCmdPool *externalcmd.Pool,
rtspAddress string,
readTimeout conf.StringDuration,
writeTimeout conf.StringDuration,
readBufferCount int,
readBufferSize int,
pathConfs map[string]*conf.PathConf,
externalCmdPool *externalcmd.Pool,
metrics *metrics,
parent pathManagerParent) *pathManager {
ctx, ctxCancel := context.WithCancel(parentCtx)
pm := &pathManager{
externalCmdPool: externalCmdPool,
rtspAddress: rtspAddress,
readTimeout: readTimeout,
writeTimeout: writeTimeout,
readBufferCount: readBufferCount,
readBufferSize: readBufferSize,
pathConfs: pathConfs,
externalCmdPool: externalCmdPool,
metrics: metrics,
parent: parent,
ctx: ctx,
@ -85,9 +82,9 @@ func newPathManager( @@ -85,9 +82,9 @@ func newPathManager(
apiPathsList: make(chan pathAPIPathsListReq),
}
for pathName, pathConf := range pm.pathConfs {
for pathConfName, pathConf := range pm.pathConfs {
if pathConf.Regexp == nil {
pm.createPath(pathName, pathConf, pathName, nil)
pm.createPath(pathConfName, pathConf, pathConfName, nil)
}
}
@ -119,23 +116,23 @@ outer: @@ -119,23 +116,23 @@ outer:
select {
case pathConfs := <-pm.confReload:
// remove confs
for pathName := range pm.pathConfs {
if _, ok := pathConfs[pathName]; !ok {
delete(pm.pathConfs, pathName)
for pathConfName := range pm.pathConfs {
if _, ok := pathConfs[pathConfName]; !ok {
delete(pm.pathConfs, pathConfName)
}
}
// update confs
for pathName, oldConf := range pm.pathConfs {
if !oldConf.Equal(pathConfs[pathName]) {
pm.pathConfs[pathName] = pathConfs[pathName]
for pathConfName, oldConf := range pm.pathConfs {
if !oldConf.Equal(pathConfs[pathConfName]) {
pm.pathConfs[pathConfName] = pathConfs[pathConfName]
}
}
// add confs
for pathName, pathConf := range pathConfs {
if _, ok := pm.pathConfs[pathName]; !ok {
pm.pathConfs[pathName] = pathConf
for pathConfName, pathConf := range pathConfs {
if _, ok := pm.pathConfs[pathConfName]; !ok {
pm.pathConfs[pathConfName] = pathConf
}
}
@ -149,9 +146,9 @@ outer: @@ -149,9 +146,9 @@ outer:
}
// add new paths
for pathName, pathConf := range pm.pathConfs {
if _, ok := pm.paths[pathName]; !ok && pathConf.Regexp == nil {
pm.createPath(pathName, pathConf, pathName, nil)
for pathConfName, pathConf := range pm.pathConfs {
if _, ok := pm.paths[pathConfName]; !ok && pathConf.Regexp == nil {
pm.createPath(pathConfName, pathConf, pathConfName, nil)
}
}
@ -168,20 +165,16 @@ outer: @@ -168,20 +165,16 @@ outer:
}
case req := <-pm.describe:
pathName, pathConf, pathMatches, err := pm.findPathConf(req.PathName)
pathConfName, pathConf, pathMatches, err := pm.findPathConf(req.PathName)
if err != nil {
req.Res <- pathDescribeRes{Err: err}
continue
}
err = pm.authenticate(
req.IP,
req.ValidateCredentials,
req.PathName,
err = req.Authenticate(
pathConf.ReadIPs,
pathConf.ReadUser,
pathConf.ReadPass,
)
pathConf.ReadPass)
if err != nil {
req.Res <- pathDescribeRes{Err: err}
continue
@ -189,53 +182,47 @@ outer: @@ -189,53 +182,47 @@ outer:
// create path if it doesn't exist
if _, ok := pm.paths[req.PathName]; !ok {
pm.createPath(pathName, pathConf, req.PathName, pathMatches)
pm.createPath(pathConfName, pathConf, req.PathName, pathMatches)
}
req.Res <- pathDescribeRes{Path: pm.paths[req.PathName]}
case req := <-pm.readerSetupPlay:
pathName, pathConf, pathMatches, err := pm.findPathConf(req.PathName)
pathConfName, pathConf, pathMatches, err := pm.findPathConf(req.PathName)
if err != nil {
req.Res <- pathReaderSetupPlayRes{Err: err}
continue
}
err = pm.authenticate(
req.IP,
req.ValidateCredentials,
req.PathName,
pathConf.ReadIPs,
pathConf.ReadUser,
pathConf.ReadPass,
)
if err != nil {
req.Res <- pathReaderSetupPlayRes{Err: err}
continue
if req.Authenticate != nil {
err = req.Authenticate(
pathConf.ReadIPs,
pathConf.ReadUser,
pathConf.ReadPass)
if err != nil {
req.Res <- pathReaderSetupPlayRes{Err: err}
continue
}
}
// create path if it doesn't exist
if _, ok := pm.paths[req.PathName]; !ok {
pm.createPath(pathName, pathConf, req.PathName, pathMatches)
pm.createPath(pathConfName, pathConf, req.PathName, pathMatches)
}
req.Res <- pathReaderSetupPlayRes{Path: pm.paths[req.PathName]}
case req := <-pm.publisherAnnounce:
pathName, pathConf, pathMatches, err := pm.findPathConf(req.PathName)
pathConfName, pathConf, pathMatches, err := pm.findPathConf(req.PathName)
if err != nil {
req.Res <- pathPublisherAnnounceRes{Err: err}
continue
}
err = pm.authenticate(
req.IP,
req.ValidateCredentials,
req.PathName,
err = req.Authenticate(
pathConf.PublishIPs,
pathConf.PublishUser,
pathConf.PublishPass,
)
pathConf.PublishPass)
if err != nil {
req.Res <- pathPublisherAnnounceRes{Err: err}
continue
@ -243,7 +230,7 @@ outer: @@ -243,7 +230,7 @@ outer:
// create path if it doesn't exist
if _, ok := pm.paths[req.PathName]; !ok {
pm.createPath(pathName, pathConf, req.PathName, pathMatches)
pm.createPath(pathConfName, pathConf, req.PathName, pathMatches)
}
req.Res <- pathPublisherAnnounceRes{Path: pm.paths[req.PathName]}
@ -275,23 +262,23 @@ outer: @@ -275,23 +262,23 @@ outer:
}
func (pm *pathManager) createPath(
confName string,
conf *conf.PathConf,
pathConfName string,
pathConf *conf.PathConf,
name string,
matches []string) {
pm.paths[name] = newPath(
pm.ctx,
pm.externalCmdPool,
pm.rtspAddress,
pm.readTimeout,
pm.writeTimeout,
pm.readBufferCount,
pm.readBufferSize,
confName,
conf,
pathConfName,
pathConf,
name,
matches,
&pm.wg,
pm.externalCmdPool,
pm)
}
@ -307,11 +294,11 @@ func (pm *pathManager) findPathConf(name string) (string, *conf.PathConf, []stri @@ -307,11 +294,11 @@ func (pm *pathManager) findPathConf(name string) (string, *conf.PathConf, []stri
}
// regular expression path
for pathName, pathConf := range pm.pathConfs {
for pathConfName, pathConf := range pm.pathConfs {
if pathConf.Regexp != nil {
m := pathConf.Regexp.FindStringSubmatch(name)
if m != nil {
return pathName, pathConf, m, nil
return pathConfName, pathConf, m, nil
}
}
}
@ -319,37 +306,6 @@ func (pm *pathManager) findPathConf(name string) (string, *conf.PathConf, []stri @@ -319,37 +306,6 @@ func (pm *pathManager) findPathConf(name string) (string, *conf.PathConf, []stri
return "", nil, nil, fmt.Errorf("path '%s' is not configured", name)
}
func (pm *pathManager) authenticate(
ip net.IP,
validateCredentials func(pathUser conf.Credential, pathPass conf.Credential) error,
pathName string,
pathIPs []interface{},
pathUser conf.Credential,
pathPass conf.Credential,
) error {
// validate ip
if pathIPs != nil && ip != nil {
if !ipEqualOrInRange(ip, pathIPs) {
return pathErrAuthCritical{
Message: fmt.Sprintf("IP '%s' not allowed", ip),
Response: &base.Response{
StatusCode: base.StatusUnauthorized,
},
}
}
}
// validate user
if pathUser != "" && validateCredentials != nil {
err := validateCredentials(pathUser, pathPass)
if err != nil {
return err
}
}
return nil
}
// onConfReload is called by core.
func (pm *pathManager) onConfReload(pathConfs map[string]*conf.PathConf) {
select {

112
internal/core/rtmp_conn.go

@ -53,18 +53,19 @@ type rtmpConnParent interface { @@ -53,18 +53,19 @@ type rtmpConnParent interface {
}
type rtmpConn struct {
externalCmdPool *externalcmd.Pool
id string
rtspAddress string
readTimeout conf.StringDuration
writeTimeout conf.StringDuration
readBufferCount int
runOnConnect string
runOnConnectRestart bool
wg *sync.WaitGroup
conn *rtmp.Conn
pathManager rtmpConnPathManager
parent rtmpConnParent
id string
externalAuthenticationURL string
rtspAddress string
readTimeout conf.StringDuration
writeTimeout conf.StringDuration
readBufferCount int
runOnConnect string
runOnConnectRestart bool
wg *sync.WaitGroup
conn *rtmp.Conn
externalCmdPool *externalcmd.Pool
pathManager rtmpConnPathManager
parent rtmpConnParent
ctx context.Context
ctxCancel func()
@ -76,8 +77,8 @@ type rtmpConn struct { @@ -76,8 +77,8 @@ type rtmpConn struct {
func newRTMPConn(
parentCtx context.Context,
externalCmdPool *externalcmd.Pool,
id string,
externalAuthenticationURL string,
rtspAddress string,
readTimeout conf.StringDuration,
writeTimeout conf.StringDuration,
@ -86,25 +87,27 @@ func newRTMPConn( @@ -86,25 +87,27 @@ func newRTMPConn(
runOnConnectRestart bool,
wg *sync.WaitGroup,
nconn net.Conn,
externalCmdPool *externalcmd.Pool,
pathManager rtmpConnPathManager,
parent rtmpConnParent) *rtmpConn {
ctx, ctxCancel := context.WithCancel(parentCtx)
c := &rtmpConn{
externalCmdPool: externalCmdPool,
id: id,
rtspAddress: rtspAddress,
readTimeout: readTimeout,
writeTimeout: writeTimeout,
readBufferCount: readBufferCount,
runOnConnect: runOnConnect,
runOnConnectRestart: runOnConnectRestart,
wg: wg,
conn: rtmp.NewServerConn(nconn),
pathManager: pathManager,
parent: parent,
ctx: ctx,
ctxCancel: ctxCancel,
id: id,
externalAuthenticationURL: externalAuthenticationURL,
rtspAddress: rtspAddress,
readTimeout: readTimeout,
writeTimeout: writeTimeout,
readBufferCount: readBufferCount,
runOnConnect: runOnConnect,
runOnConnectRestart: runOnConnectRestart,
wg: wg,
conn: rtmp.NewServerConn(nconn),
externalCmdPool: externalCmdPool,
pathManager: pathManager,
parent: parent,
ctx: ctx,
ctxCancel: ctxCancel,
}
c.log(logger.Info, "opened")
@ -219,9 +222,11 @@ func (c *rtmpConn) runRead(ctx context.Context) error { @@ -219,9 +222,11 @@ func (c *rtmpConn) runRead(ctx context.Context) error {
res := c.pathManager.onReaderSetupPlay(pathReaderSetupPlayReq{
Author: c,
PathName: pathName,
IP: c.ip(),
ValidateCredentials: func(pathUser conf.Credential, pathPass conf.Credential) error {
return c.validateCredentials(pathUser, pathPass, query)
Authenticate: func(
pathIPs []interface{},
pathUser conf.Credential,
pathPass conf.Credential) error {
return c.authenticate(pathName, pathIPs, pathUser, pathPass, "read", query)
},
})
@ -462,9 +467,11 @@ func (c *rtmpConn) runPublish(ctx context.Context) error { @@ -462,9 +467,11 @@ func (c *rtmpConn) runPublish(ctx context.Context) error {
res := c.pathManager.onPublisherAnnounce(pathPublisherAnnounceReq{
Author: c,
PathName: pathName,
IP: c.ip(),
ValidateCredentials: func(pathUser conf.Credential, pathPass conf.Credential) error {
return c.validateCredentials(pathUser, pathPass, query)
Authenticate: func(
pathIPs []interface{},
pathUser conf.Credential,
pathPass conf.Credential) error {
return c.authenticate(pathName, pathIPs, pathUser, pathPass, "publish", query)
},
})
@ -585,15 +592,44 @@ func (c *rtmpConn) runPublish(ctx context.Context) error { @@ -585,15 +592,44 @@ func (c *rtmpConn) runPublish(ctx context.Context) error {
}
}
func (c *rtmpConn) validateCredentials(
func (c *rtmpConn) authenticate(
pathName string,
pathIPs []interface{},
pathUser conf.Credential,
pathPass conf.Credential,
action string,
query url.Values,
) error {
if query.Get("user") != string(pathUser) ||
query.Get("pass") != string(pathPass) {
return pathErrAuthCritical{
Message: "wrong username or password",
if c.externalAuthenticationURL != "" {
err := externalAuth(
c.externalAuthenticationURL,
c.ip().String(),
query.Get("user"),
query.Get("pass"),
pathName,
action)
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",
}
}
}

62
internal/core/rtmp_server.go

@ -48,16 +48,17 @@ type rtmpServerParent interface { @@ -48,16 +48,17 @@ type rtmpServerParent interface {
}
type rtmpServer struct {
externalCmdPool *externalcmd.Pool
readTimeout conf.StringDuration
writeTimeout conf.StringDuration
readBufferCount int
rtspAddress string
runOnConnect string
runOnConnectRestart bool
metrics *metrics
pathManager *pathManager
parent rtmpServerParent
externalAuthenticationURL string
readTimeout conf.StringDuration
writeTimeout conf.StringDuration
readBufferCount int
rtspAddress string
runOnConnect string
runOnConnectRestart bool
externalCmdPool *externalcmd.Pool
metrics *metrics
pathManager *pathManager
parent rtmpServerParent
ctx context.Context
ctxCancel func()
@ -73,7 +74,7 @@ type rtmpServer struct { @@ -73,7 +74,7 @@ type rtmpServer struct {
func newRTMPServer(
parentCtx context.Context,
externalCmdPool *externalcmd.Pool,
externalAuthenticationURL string,
address string,
readTimeout conf.StringDuration,
writeTimeout conf.StringDuration,
@ -81,6 +82,7 @@ func newRTMPServer( @@ -81,6 +82,7 @@ func newRTMPServer(
rtspAddress string,
runOnConnect string,
runOnConnectRestart bool,
externalCmdPool *externalcmd.Pool,
metrics *metrics,
pathManager *pathManager,
parent rtmpServerParent) (*rtmpServer, error) {
@ -92,23 +94,24 @@ func newRTMPServer( @@ -92,23 +94,24 @@ func newRTMPServer(
ctx, ctxCancel := context.WithCancel(parentCtx)
s := &rtmpServer{
externalCmdPool: externalCmdPool,
readTimeout: readTimeout,
writeTimeout: writeTimeout,
readBufferCount: readBufferCount,
rtspAddress: rtspAddress,
runOnConnect: runOnConnect,
runOnConnectRestart: runOnConnectRestart,
metrics: metrics,
pathManager: pathManager,
parent: parent,
ctx: ctx,
ctxCancel: ctxCancel,
l: l,
conns: make(map[*rtmpConn]struct{}),
connClose: make(chan *rtmpConn),
apiConnsList: make(chan rtmpServerAPIConnsListReq),
apiConnsKick: make(chan rtmpServerAPIConnsKickReq),
externalAuthenticationURL: externalAuthenticationURL,
readTimeout: readTimeout,
writeTimeout: writeTimeout,
readBufferCount: readBufferCount,
rtspAddress: rtspAddress,
runOnConnect: runOnConnect,
runOnConnectRestart: runOnConnectRestart,
externalCmdPool: externalCmdPool,
metrics: metrics,
pathManager: pathManager,
parent: parent,
ctx: ctx,
ctxCancel: ctxCancel,
l: l,
conns: make(map[*rtmpConn]struct{}),
connClose: make(chan *rtmpConn),
apiConnsList: make(chan rtmpServerAPIConnsListReq),
apiConnsKick: make(chan rtmpServerAPIConnsKickReq),
}
s.log(logger.Info, "listener opened on %s", address)
@ -174,8 +177,8 @@ outer: @@ -174,8 +177,8 @@ outer:
c := newRTMPConn(
s.ctx,
s.externalCmdPool,
id,
s.externalAuthenticationURL,
s.rtspAddress,
s.readTimeout,
s.writeTimeout,
@ -184,6 +187,7 @@ outer: @@ -184,6 +187,7 @@ outer:
s.runOnConnectRestart,
&s.wg,
nconn,
s.externalCmdPool,
s.pathManager,
s)
s.conns[c] = struct{}{}

161
internal/core/rtmp_server_test.go

@ -1,10 +1,14 @@ @@ -1,10 +1,14 @@
package core
import (
"context"
"io"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/aler9/rtsp-simple-server/internal/rtmp"
)
func TestRTMPServerPublish(t *testing.T) {
@ -78,49 +82,80 @@ func TestRTMPServerRead(t *testing.T) { @@ -78,49 +82,80 @@ func TestRTMPServerRead(t *testing.T) {
}
func TestRTMPServerAuth(t *testing.T) {
t.Run("publish", func(t *testing.T) {
p, ok := newInstance("rtspDisable: yes\n" +
"hlsDisable: yes\n" +
"paths:\n" +
" all:\n" +
" publishUser: testuser\n" +
" publishPass: testpass\n" +
" readIPs: [127.0.0.0/16]\n")
require.Equal(t, true, ok)
defer p.close()
for _, ca := range []string{
"internal",
"external",
} {
t.Run(ca, func(t *testing.T) {
var conf string
if ca == "internal" {
conf = "paths:\n" +
" all:\n" +
" publishUser: testpublisher\n" +
" publishPass: testpass\n" +
" publishIPs: [127.0.0.0/16]\n" +
" readUser: testreader\n" +
" readPass: testpass\n" +
" readIPs: [127.0.0.0/16]\n"
} else {
conf = "externalAuthenticationURL: http://localhost:9120/auth\n" +
"paths:\n" +
" all:\n"
}
p, ok := newInstance(conf)
require.Equal(t, true, ok)
defer p.close()
cnt1, err := newContainer("ffmpeg", "source", []string{
"-re",
"-stream_loop", "-1",
"-i", "emptyvideo.mkv",
"-c", "copy",
"-f", "flv",
"rtmp://localhost/teststream?user=testuser&pass=testpass",
})
require.NoError(t, err)
defer cnt1.close()
var a *testHTTPAuthenticator
if ca == "external" {
var err error
a, err = newTestHTTPAuthenticator("publish")
require.NoError(t, err)
}
time.Sleep(1 * time.Second)
cnt1, err := newContainer("ffmpeg", "source", []string{
"-re",
"-stream_loop", "-1",
"-i", "emptyvideo.mkv",
"-c", "copy",
"-f", "flv",
"rtmp://127.0.0.1/teststream?user=testpublisher&pass=testpass",
})
require.NoError(t, err)
defer cnt1.close()
time.Sleep(1 * time.Second)
if ca == "external" {
a.close()
a, err = newTestHTTPAuthenticator("read")
require.NoError(t, err)
defer a.close()
}
conn, err := rtmp.DialContext(context.Background(),
"rtmp://127.0.0.1/teststream?user=testreader&pass=testpass")
require.NoError(t, err)
defer conn.Close()
err = conn.ClientHandshake()
require.NoError(t, err)
cnt2, err := newContainer("ffmpeg", "dest", []string{
"-i", "rtmp://127.0.0.1/teststream",
"-vframes", "1",
"-f", "image2",
"-y", "/dev/null",
_, _, err = conn.ReadMetadata()
require.NoError(t, err)
})
require.NoError(t, err)
defer cnt2.close()
require.Equal(t, 0, cnt2.wait())
})
}
}
t.Run("read", func(t *testing.T) {
func TestRTMPServerAuthFail(t *testing.T) {
t.Run("publish", func(t *testing.T) {
p, ok := newInstance("rtspDisable: yes\n" +
"hlsDisable: yes\n" +
"paths:\n" +
" all:\n" +
" readUser: testuser\n" +
" readPass: testpass\n" +
" readIPs: [127.0.0.0/16]\n")
" publishUser: testuser2\n" +
" publishPass: testpass\n")
require.Equal(t, true, ok)
defer p.close()
@ -130,58 +165,35 @@ func TestRTMPServerAuth(t *testing.T) { @@ -130,58 +165,35 @@ func TestRTMPServerAuth(t *testing.T) {
"-i", "emptyvideo.mkv",
"-c", "copy",
"-f", "flv",
"rtmp://localhost/teststream",
"rtmp://localhost/teststream?user=testuser&pass=testpass",
})
require.NoError(t, err)
defer cnt1.close()
time.Sleep(1 * time.Second)
cnt2, err := newContainer("ffmpeg", "dest", []string{
"-i", "rtmp://127.0.0.1/teststream?user=testuser&pass=testpass",
"-vframes", "1",
"-f", "image2",
"-y", "/dev/null",
})
require.NoError(t, err)
defer cnt2.close()
require.Equal(t, 0, cnt2.wait())
require.NotEqual(t, 0, cnt1.wait())
})
}
func TestRTMPServerAuthFail(t *testing.T) {
t.Run("publish", func(t *testing.T) {
p, ok := newInstance("rtspDisable: yes\n" +
"hlsDisable: yes\n" +
t.Run("publish_external", func(t *testing.T) {
p, ok := newInstance("externalAuthenticationURL: http://localhost:9120/auth\n" +
"paths:\n" +
" all:\n" +
" publishUser: testuser2\n" +
" publishPass: testpass\n")
" all:\n")
require.Equal(t, true, ok)
defer p.close()
a, err := newTestHTTPAuthenticator("publish")
require.NoError(t, err)
defer a.close()
cnt1, err := newContainer("ffmpeg", "source", []string{
"-re",
"-stream_loop", "-1",
"-i", "emptyvideo.mkv",
"-c", "copy",
"-f", "flv",
"rtmp://localhost/teststream?user=testuser&pass=testpass",
"rtmp://localhost/teststream?user=testuser2&pass=testpass",
})
require.NoError(t, err)
defer cnt1.close()
time.Sleep(1 * time.Second)
cnt2, err := newContainer("ffmpeg", "dest", []string{
"-i", "rtmp://localhost/teststream",
"-vframes", "1",
"-f", "image2",
"-y", "/dev/null",
})
require.NoError(t, err)
defer cnt2.close()
require.NotEqual(t, 0, cnt2.wait())
require.NotEqual(t, 0, cnt1.wait())
})
t.Run("read", func(t *testing.T) {
@ -207,14 +219,11 @@ func TestRTMPServerAuthFail(t *testing.T) { @@ -207,14 +219,11 @@ func TestRTMPServerAuthFail(t *testing.T) {
time.Sleep(1 * time.Second)
cnt2, err := newContainer("ffmpeg", "dest", []string{
"-i", "rtmp://localhost/teststream?user=testuser&pass=testpass",
"-vframes", "1",
"-f", "image2",
"-y", "/dev/null",
})
conn, err := rtmp.DialContext(context.Background(), "rtmp://127.0.0.1/teststream?user=testuser&pass=testpass")
require.NoError(t, err)
defer cnt2.close()
require.NotEqual(t, 0, cnt2.wait())
defer conn.Close()
err = conn.ClientHandshake()
require.Equal(t, err, io.EOF)
})
}

1
internal/core/rtmp_source.go

@ -123,7 +123,6 @@ func (s *rtmpSource) runInner() bool { @@ -123,7 +123,6 @@ func (s *rtmpSource) runInner() bool {
}
conn.SetWriteDeadline(time.Time{})
conn.SetReadDeadline(time.Now().Add(time.Duration(s.readTimeout)))
videoTrack, audioTrack, err := conn.ReadMetadata()
if err != nil {

172
internal/core/rtsp_conn.go

@ -2,6 +2,7 @@ package core @@ -2,6 +2,7 @@ package core
import (
"errors"
"fmt"
"net"
"time"
@ -24,15 +25,16 @@ type rtspConnParent interface { @@ -24,15 +25,16 @@ type rtspConnParent interface {
}
type rtspConn struct {
externalCmdPool *externalcmd.Pool
rtspAddress string
authMethods []headers.AuthMethod
readTimeout conf.StringDuration
runOnConnect string
runOnConnectRestart bool
pathManager *pathManager
conn *gortsplib.ServerConn
parent rtspConnParent
externalAuthenticationURL string
rtspAddress string
authMethods []headers.AuthMethod
readTimeout conf.StringDuration
runOnConnect string
runOnConnectRestart bool
externalCmdPool *externalcmd.Pool
pathManager *pathManager
conn *gortsplib.ServerConn
parent rtspConnParent
onConnectCmd *externalcmd.Cmd
authUser string
@ -42,25 +44,27 @@ type rtspConn struct { @@ -42,25 +44,27 @@ type rtspConn struct {
}
func newRTSPConn(
externalCmdPool *externalcmd.Pool,
externalAuthenticationURL string,
rtspAddress string,
authMethods []headers.AuthMethod,
readTimeout conf.StringDuration,
runOnConnect string,
runOnConnectRestart bool,
externalCmdPool *externalcmd.Pool,
pathManager *pathManager,
conn *gortsplib.ServerConn,
parent rtspConnParent) *rtspConn {
c := &rtspConn{
externalCmdPool: externalCmdPool,
rtspAddress: rtspAddress,
authMethods: authMethods,
readTimeout: readTimeout,
runOnConnect: runOnConnect,
runOnConnectRestart: runOnConnectRestart,
pathManager: pathManager,
conn: conn,
parent: parent,
externalAuthenticationURL: externalAuthenticationURL,
rtspAddress: rtspAddress,
authMethods: authMethods,
readTimeout: readTimeout,
runOnConnect: runOnConnect,
runOnConnectRestart: runOnConnectRestart,
externalCmdPool: externalCmdPool,
pathManager: pathManager,
conn: conn,
parent: parent,
}
c.log(logger.Info, "opened")
@ -97,53 +101,117 @@ func (c *rtspConn) ip() net.IP { @@ -97,53 +101,117 @@ func (c *rtspConn) ip() net.IP {
return c.conn.NetConn().RemoteAddr().(*net.TCPAddr).IP
}
func (c *rtspConn) validateCredentials(
func (c *rtspConn) authenticate(
pathName string,
pathIPs []interface{},
pathUser conf.Credential,
pathPass conf.Credential,
action string,
req *base.Request,
) error {
// 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)
if c.externalAuthenticationURL != "" {
username := ""
password := ""
var auth headers.Authorization
err := auth.Read(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,
pathName,
action)
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{
Response: &base.Response{
StatusCode: base.StatusUnauthorized,
Header: base.Header{
"WWW-Authenticate": headers.Authenticate{
Method: headers.AuthBasic,
Realm: &v,
}.Write(),
},
},
}
}
}
err := c.authValidator.ValidateRequest(req)
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 {
if pathIPs != nil {
ip := c.ip()
if !ipEqualOrInRange(ip, pathIPs) {
return pathErrAuthCritical{
Message: "unauthorized: " + err.Error(),
Message: fmt.Sprintf("IP '%s' not allowed", ip),
Response: &base.Response{
StatusCode: base.StatusUnauthorized,
},
}
}
}
if c.authFailures > 1 {
c.log(logger.Debug, "WARN: unauthorized: %s", err)
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)
}
return pathErrAuthNotCritical{
Response: &base.Response{
StatusCode: base.StatusUnauthorized,
Header: base.Header{
"WWW-Authenticate": c.authValidator.Header(),
err := c.authValidator.ValidateRequest(req)
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
// login successful, reset authFailures
c.authFailures = 0
}
return nil
}
@ -174,9 +242,11 @@ func (c *rtspConn) onDescribe(ctx *gortsplib.ServerHandlerOnDescribeCtx, @@ -174,9 +242,11 @@ func (c *rtspConn) onDescribe(ctx *gortsplib.ServerHandlerOnDescribeCtx,
res := c.pathManager.onDescribe(pathDescribeReq{
PathName: ctx.Path,
URL: ctx.Req.URL,
IP: c.ip(),
ValidateCredentials: func(pathUser conf.Credential, pathPass conf.Credential) error {
return c.validateCredentials(pathUser, pathPass, ctx.Req)
Authenticate: func(
pathIPs []interface{},
pathUser conf.Credential,
pathPass conf.Credential) error {
return c.authenticate(ctx.Path, pathIPs, pathUser, pathPass, "read", ctx.Req)
},
})

58
internal/core/rtsp_server.go

@ -50,17 +50,18 @@ type rtspServerParent interface { @@ -50,17 +50,18 @@ type rtspServerParent interface {
}
type rtspServer struct {
externalCmdPool *externalcmd.Pool
authMethods []headers.AuthMethod
readTimeout conf.StringDuration
isTLS bool
rtspAddress string
protocols map[conf.Protocol]struct{}
runOnConnect string
runOnConnectRestart bool
metrics *metrics
pathManager *pathManager
parent rtspServerParent
externalAuthenticationURL string
authMethods []headers.AuthMethod
readTimeout conf.StringDuration
isTLS bool
rtspAddress string
protocols map[conf.Protocol]struct{}
runOnConnect string
runOnConnectRestart bool
externalCmdPool *externalcmd.Pool
metrics *metrics
pathManager *pathManager
parent rtspServerParent
ctx context.Context
ctxCancel func()
@ -73,7 +74,7 @@ type rtspServer struct { @@ -73,7 +74,7 @@ type rtspServer struct {
func newRTSPServer(
parentCtx context.Context,
externalCmdPool *externalcmd.Pool,
externalAuthenticationURL string,
address string,
authMethods []headers.AuthMethod,
readTimeout conf.StringDuration,
@ -94,25 +95,27 @@ func newRTSPServer( @@ -94,25 +95,27 @@ func newRTSPServer(
protocols map[conf.Protocol]struct{},
runOnConnect string,
runOnConnectRestart bool,
externalCmdPool *externalcmd.Pool,
metrics *metrics,
pathManager *pathManager,
parent rtspServerParent) (*rtspServer, error) {
ctx, ctxCancel := context.WithCancel(parentCtx)
s := &rtspServer{
externalCmdPool: externalCmdPool,
authMethods: authMethods,
readTimeout: readTimeout,
isTLS: isTLS,
rtspAddress: rtspAddress,
protocols: protocols,
metrics: metrics,
pathManager: pathManager,
parent: parent,
ctx: ctx,
ctxCancel: ctxCancel,
conns: make(map[*gortsplib.ServerConn]*rtspConn),
sessions: make(map[*gortsplib.ServerSession]*rtspSession),
externalAuthenticationURL: externalAuthenticationURL,
authMethods: authMethods,
readTimeout: readTimeout,
isTLS: isTLS,
rtspAddress: rtspAddress,
protocols: protocols,
externalCmdPool: externalCmdPool,
metrics: metrics,
pathManager: pathManager,
parent: parent,
ctx: ctx,
ctxCancel: ctxCancel,
conns: make(map[*gortsplib.ServerConn]*rtspConn),
sessions: make(map[*gortsplib.ServerSession]*rtspSession),
}
s.srv = &gortsplib.Server{
@ -255,12 +258,13 @@ func (s *rtspServer) newSessionID() (string, error) { @@ -255,12 +258,13 @@ func (s *rtspServer) newSessionID() (string, error) {
// OnConnOpen implements gortsplib.ServerHandlerOnConnOpen.
func (s *rtspServer) OnConnOpen(ctx *gortsplib.ServerHandlerOnConnOpenCtx) {
c := newRTSPConn(
s.externalCmdPool,
s.externalAuthenticationURL,
s.rtspAddress,
s.authMethods,
s.readTimeout,
s.runOnConnect,
s.runOnConnectRestart,
s.externalCmdPool,
s.pathManager,
ctx.Conn,
s)
@ -305,12 +309,12 @@ func (s *rtspServer) OnSessionOpen(ctx *gortsplib.ServerHandlerOnSessionOpenCtx) @@ -305,12 +309,12 @@ func (s *rtspServer) OnSessionOpen(ctx *gortsplib.ServerHandlerOnSessionOpenCtx)
id, _ := s.newSessionID()
se := newRTSPSession(
s.externalCmdPool,
s.isTLS,
s.protocols,
id,
ctx.Session,
ctx.Conn,
s.externalCmdPool,
s.pathManager,
s)

137
internal/core/rtsp_server_test.go

@ -195,78 +195,65 @@ func TestRTSPServerPublishRead(t *testing.T) { @@ -195,78 +195,65 @@ func TestRTSPServerPublishRead(t *testing.T) {
}
func TestRTSPServerAuth(t *testing.T) {
t.Run("publish", func(t *testing.T) {
p, ok := newInstance("rtmpDisable: yes\n" +
"hlsDisable: yes\n" +
"paths:\n" +
" all:\n" +
" publishUser: testuser\n" +
" publishPass: test!$()*+.;<=>[]^_-{}\n" +
" publishIPs: [127.0.0.0/16]\n")
require.Equal(t, true, ok)
defer p.close()
track, err := gortsplib.NewTrackH264(96,
&gortsplib.TrackConfigH264{SPS: []byte{0x01, 0x02, 0x03, 0x04}, PPS: []byte{0x01, 0x02, 0x03, 0x04}})
require.NoError(t, err)
source := gortsplib.Client{}
err = source.StartPublishing(
"rtsp://testuser:test%21%24%28%29%2A%2B.%3B%3C%3D%3E%5B%5D%5E_-%7B%7D@127.0.0.1:8554/test/stream",
gortsplib.Tracks{track})
require.NoError(t, err)
defer source.Close()
})
for _, soft := range []string{
"ffmpeg",
"vlc",
for _, ca := range []string{
"internal",
"external",
} {
t.Run("read_"+soft, func(t *testing.T) {
p, ok := newInstance("rtmpDisable: yes\n" +
"hlsDisable: yes\n" +
"paths:\n" +
" all:\n" +
" readUser: testuser\n" +
" readPass: test!$()*+.;<=>[]^_-{}\n" +
" readIPs: [127.0.0.0/16]\n")
t.Run(ca, func(t *testing.T) {
var conf string
if ca == "internal" {
conf = "rtmpDisable: yes\n" +
"hlsDisable: yes\n" +
"paths:\n" +
" all:\n" +
" publishUser: testpublisher\n" +
" publishPass: testpass\n" +
" publishIPs: [127.0.0.0/16]\n" +
" readUser: testreader\n" +
" readPass: testpass\n" +
" readIPs: [127.0.0.0/16]\n"
} else {
conf = "externalAuthenticationURL: http://localhost:9120/auth\n" +
"paths:\n" +
" all:\n"
}
p, ok := newInstance(conf)
require.Equal(t, true, ok)
defer p.close()
cnt1, err := newContainer("ffmpeg", "source", []string{
"-re",
"-stream_loop", "-1",
"-i", "emptyvideo.mkv",
"-c", "copy",
"-f", "rtsp",
"-rtsp_transport", "udp",
"rtsp://localhost:8554/test/stream",
})
var a *testHTTPAuthenticator
if ca == "external" {
var err error
a, err = newTestHTTPAuthenticator("publish")
require.NoError(t, err)
}
track, err := gortsplib.NewTrackH264(96,
&gortsplib.TrackConfigH264{SPS: []byte{0x01, 0x02, 0x03, 0x04}, PPS: []byte{0x01, 0x02, 0x03, 0x04}})
require.NoError(t, err)
defer cnt1.close()
time.Sleep(1 * time.Second)
source := gortsplib.Client{}
if soft == "ffmpeg" {
cnt2, err := newContainer("ffmpeg", "dest", []string{
"-rtsp_transport", "udp",
"-i", "rtsp://testuser:test!$()*+.;<=>[]^_-{}@127.0.0.1:8554/test/stream",
"-vframes", "1",
"-f", "image2",
"-y", "/dev/null",
})
require.NoError(t, err)
defer cnt2.close()
require.Equal(t, 0, cnt2.wait())
} else {
cnt2, err := newContainer("vlc", "dest", []string{
"rtsp://testuser:test!$()*+.;<=>[]^_-{}@localhost:8554/test/stream",
})
err = source.StartPublishing(
"rtsp://testpublisher:testpass@127.0.0.1:8554/teststream",
gortsplib.Tracks{track})
require.NoError(t, err)
defer source.Close()
if ca == "external" {
a.close()
var err error
a, err = newTestHTTPAuthenticator("read")
require.NoError(t, err)
defer cnt2.close()
require.Equal(t, 0, cnt2.wait())
defer a.close()
}
reader := gortsplib.Client{}
err = reader.StartReading("rtsp://testreader:testpass@127.0.0.1:8554/teststream")
require.NoError(t, err)
defer reader.Close()
})
}
@ -401,6 +388,30 @@ func TestRTSPServerAuthFail(t *testing.T) { @@ -401,6 +388,30 @@ func TestRTSPServerAuthFail(t *testing.T) {
)
require.EqualError(t, err, "bad status code: 401 (Unauthorized)")
})
t.Run("external", func(t *testing.T) {
p, ok := newInstance("externalAuthenticationURL: http://localhost:9120/auth\n" +
"paths:\n" +
" all:\n")
require.Equal(t, true, ok)
defer p.close()
a, err := newTestHTTPAuthenticator("publish")
require.NoError(t, err)
defer a.close()
track, err := gortsplib.NewTrackH264(96,
&gortsplib.TrackConfigH264{SPS: []byte{0x01, 0x02, 0x03, 0x04}, PPS: []byte{0x01, 0x02, 0x03, 0x04}})
require.NoError(t, err)
c := gortsplib.Client{}
err = c.StartPublishing(
"rtsp://testpublisher2:testpass@localhost:8554/teststream",
gortsplib.Tracks{track},
)
require.EqualError(t, err, "bad status code: 401 (Unauthorized)")
})
}
func TestRTSPServerPublisherOverride(t *testing.T) {

22
internal/core/rtsp_session.go

@ -29,12 +29,12 @@ type rtspSessionParent interface { @@ -29,12 +29,12 @@ type rtspSessionParent interface {
}
type rtspSession struct {
externalCmdPool *externalcmd.Pool
isTLS bool
protocols map[conf.Protocol]struct{}
id string
ss *gortsplib.ServerSession
author *gortsplib.ServerConn
externalCmdPool *externalcmd.Pool
pathManager rtspSessionPathManager
parent rtspSessionParent
@ -48,21 +48,21 @@ type rtspSession struct { @@ -48,21 +48,21 @@ type rtspSession struct {
}
func newRTSPSession(
externalCmdPool *externalcmd.Pool,
isTLS bool,
protocols map[conf.Protocol]struct{},
id string,
ss *gortsplib.ServerSession,
sc *gortsplib.ServerConn,
externalCmdPool *externalcmd.Pool,
pathManager rtspSessionPathManager,
parent rtspSessionParent) *rtspSession {
s := &rtspSession{
externalCmdPool: externalCmdPool,
isTLS: isTLS,
protocols: protocols,
id: id,
ss: ss,
author: sc,
externalCmdPool: externalCmdPool,
pathManager: pathManager,
parent: parent,
}
@ -157,9 +157,11 @@ func (s *rtspSession) onAnnounce(c *rtspConn, ctx *gortsplib.ServerHandlerOnAnno @@ -157,9 +157,11 @@ func (s *rtspSession) onAnnounce(c *rtspConn, ctx *gortsplib.ServerHandlerOnAnno
res := s.pathManager.onPublisherAnnounce(pathPublisherAnnounceReq{
Author: s,
PathName: ctx.Path,
IP: ctx.Conn.NetConn().RemoteAddr().(*net.TCPAddr).IP,
ValidateCredentials: func(pathUser conf.Credential, pathPass conf.Credential) error {
return c.validateCredentials(pathUser, pathPass, ctx.Req)
Authenticate: func(
pathIPs []interface{},
pathUser conf.Credential,
pathPass conf.Credential) error {
return c.authenticate(ctx.Path, pathIPs, pathUser, pathPass, "publish", ctx.Req)
},
})
@ -213,9 +215,11 @@ func (s *rtspSession) onSetup(c *rtspConn, ctx *gortsplib.ServerHandlerOnSetupCt @@ -213,9 +215,11 @@ func (s *rtspSession) onSetup(c *rtspConn, ctx *gortsplib.ServerHandlerOnSetupCt
res := s.pathManager.onReaderSetupPlay(pathReaderSetupPlayReq{
Author: s,
PathName: ctx.Path,
IP: ctx.Conn.NetConn().RemoteAddr().(*net.TCPAddr).IP,
ValidateCredentials: func(pathUser conf.Credential, pathPass conf.Credential) error {
return c.validateCredentials(pathUser, pathPass, ctx.Req)
Authenticate: func(
pathIPs []interface{},
pathUser conf.Credential,
pathPass conf.Credential) error {
return c.authenticate(ctx.Path, pathIPs, pathUser, pathPass, "read", ctx.Req)
},
})

14
rtsp-simple-server.yml

@ -17,6 +17,20 @@ writeTimeout: 10s @@ -17,6 +17,20 @@ writeTimeout: 10s
# A higher number allows a wider throughput, a lower number allows to save RAM.
readBufferCount: 512
# HTTP URL to perform external authentication.
# Every time a user wants to authenticate, the server calls this URL
# with the POST method and a body containing:
# {
# "ip": "ip",
# "user": "user",
# "password": "password",
# "path": "path",
# "action": "read|publish"
# }
# If the response code is 20x, authentication is accepted, otherwise
# it is discarded.
externalAuthenticationURL:
# Enable the HTTP API.
api: no
# Address of the API listener.

Loading…
Cancel
Save