Browse Source

support external authentication (#504) (#517)

pull/745/head
aler9 5 years ago
parent
commit
11760fd79f
  1. 22
      README.md
  2. 2
      apidocs/openapi.yaml
  3. 11
      internal/conf/conf.go
  4. 18
      internal/conf/path.go
  5. 1
      internal/core/api.go
  6. 16
      internal/core/core.go
  7. 42
      internal/core/externalauth.go
  8. 76
      internal/core/hls_muxer.go
  9. 4
      internal/core/hls_server.go
  10. 134
      internal/core/hls_server_test.go
  11. 21
      internal/core/path.go
  12. 120
      internal/core/path_manager.go
  13. 58
      internal/core/rtmp_conn.go
  14. 12
      internal/core/rtmp_server.go
  15. 131
      internal/core/rtmp_server_test.go
  16. 1
      internal/core/rtmp_source.go
  17. 94
      internal/core/rtsp_conn.go
  18. 14
      internal/core/rtsp_server.go
  19. 113
      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:
* Each stream can have multiple video and audio tracks, encoded with any codec, including H264, H265, VP8, VP9, MPEG2, MP3, AAC, Opus, PCM, JPEG * 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 * 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 * 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 * Query and control the server through an HTTP API
* Read Prometheus-compatible metrics * Read Prometheus-compatible metrics
* Redirect readers to other RTSP servers (load balancing) * Redirect readers to other RTSP servers (load balancing)
@ -220,6 +220,26 @@ paths:
**WARNING**: enable encryption or use a VPN to ensure that no one is intercepting the credentials. **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 ### Encrypt the configuration
The configuration file can be entirely encrypted for security purposes. The configuration file can be entirely encrypted for security purposes.

2
apidocs/openapi.yaml

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

11
internal/conf/conf.go

@ -7,6 +7,7 @@ import (
"io/ioutil" "io/ioutil"
"os" "os"
"reflect" "reflect"
"strings"
"time" "time"
"github.com/aler9/gortsplib" "github.com/aler9/gortsplib"
@ -159,6 +160,7 @@ type Conf struct {
ReadTimeout StringDuration `json:"readTimeout"` ReadTimeout StringDuration `json:"readTimeout"`
WriteTimeout StringDuration `json:"writeTimeout"` WriteTimeout StringDuration `json:"writeTimeout"`
ReadBufferCount int `json:"readBufferCount"` ReadBufferCount int `json:"readBufferCount"`
ExternalAuthenticationURL string `json:"externalAuthenticationURL"`
API bool `json:"api"` API bool `json:"api"`
APIAddress string `json:"apiAddress"` APIAddress string `json:"apiAddress"`
Metrics bool `json:"metrics"` Metrics bool `json:"metrics"`
@ -248,6 +250,13 @@ func (conf *Conf) CheckAndFillMissing() error {
conf.ReadBufferCount = 512 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 == "" { if conf.APIAddress == "" {
conf.APIAddress = "127.0.0.1:9997" conf.APIAddress = "127.0.0.1:9997"
} }
@ -356,7 +365,7 @@ func (conf *Conf) CheckAndFillMissing() error {
pconf = conf.Paths[name] pconf = conf.Paths[name]
} }
err := pconf.checkAndFillMissing(name) err := pconf.checkAndFillMissing(conf, name)
if err != nil { if err != nil {
return err return err
} }

18
internal/conf/path.go

@ -71,7 +71,7 @@ type PathConf struct {
RunOnReadRestart bool `json:"runOnReadRestart"` RunOnReadRestart bool `json:"runOnReadRestart"`
} }
func (pconf *PathConf) checkAndFillMissing(name string) error { func (pconf *PathConf) checkAndFillMissing(conf *Conf, name string) error {
if name == "" { if name == "" {
return fmt.Errorf("path name can not be empty") return fmt.Errorf("path name can not be empty")
} }
@ -207,16 +207,32 @@ func (pconf *PathConf) checkAndFillMissing(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 != "" {
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 { if pconf.RunOnInit != "" && pconf.Regexp != nil {
return fmt.Errorf("a path with a regular expression does not support option 'runOnInit'; use another path") return fmt.Errorf("a path with a regular expression does not support option 'runOnInit'; use another path")
} }

1
internal/core/api.go

@ -50,6 +50,7 @@ func loadConfData(ctx *gin.Context) (interface{}, error) {
ReadTimeout *conf.StringDuration `json:"readTimeout"` ReadTimeout *conf.StringDuration `json:"readTimeout"`
WriteTimeout *conf.StringDuration `json:"writeTimeout"` WriteTimeout *conf.StringDuration `json:"writeTimeout"`
ReadBufferCount *int `json:"readBufferCount"` ReadBufferCount *int `json:"readBufferCount"`
ExternalAuthenticationURL *string `json:"externalAuthenticationURL"`
API *bool `json:"api"` API *bool `json:"api"`
APIAddress *string `json:"apiAddress"` APIAddress *string `json:"apiAddress"`
Metrics *bool `json:"metrics"` Metrics *bool `json:"metrics"`

16
internal/core/core.go

@ -220,13 +220,13 @@ 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.externalCmdPool,
p.conf.RTSPAddress, p.conf.RTSPAddress,
p.conf.ReadTimeout, p.conf.ReadTimeout,
p.conf.WriteTimeout, p.conf.WriteTimeout,
p.conf.ReadBufferCount, p.conf.ReadBufferCount,
p.conf.ReadBufferSize, p.conf.ReadBufferSize,
p.conf.Paths, p.conf.Paths,
p.externalCmdPool,
p.metrics, p.metrics,
p) p)
} }
@ -239,7 +239,7 @@ 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.externalCmdPool, p.conf.ExternalAuthenticationURL,
p.conf.RTSPAddress, p.conf.RTSPAddress,
p.conf.AuthMethods, p.conf.AuthMethods,
p.conf.ReadTimeout, p.conf.ReadTimeout,
@ -260,6 +260,7 @@ func (p *Core) createResources(initial bool) error {
p.conf.Protocols, p.conf.Protocols,
p.conf.RunOnConnect, p.conf.RunOnConnect,
p.conf.RunOnConnectRestart, p.conf.RunOnConnectRestart,
p.externalCmdPool,
p.metrics, p.metrics,
p.pathManager, p.pathManager,
p) p)
@ -275,7 +276,7 @@ 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.externalCmdPool, p.conf.ExternalAuthenticationURL,
p.conf.RTSPSAddress, p.conf.RTSPSAddress,
p.conf.AuthMethods, p.conf.AuthMethods,
p.conf.ReadTimeout, p.conf.ReadTimeout,
@ -296,6 +297,7 @@ func (p *Core) createResources(initial bool) error {
p.conf.Protocols, p.conf.Protocols,
p.conf.RunOnConnect, p.conf.RunOnConnect,
p.conf.RunOnConnectRestart, p.conf.RunOnConnectRestart,
p.externalCmdPool,
p.metrics, p.metrics,
p.pathManager, p.pathManager,
p) p)
@ -309,7 +311,7 @@ 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.externalCmdPool, p.conf.ExternalAuthenticationURL,
p.conf.RTMPAddress, p.conf.RTMPAddress,
p.conf.ReadTimeout, p.conf.ReadTimeout,
p.conf.WriteTimeout, p.conf.WriteTimeout,
@ -317,6 +319,7 @@ func (p *Core) createResources(initial bool) error {
p.conf.RTSPAddress, p.conf.RTSPAddress,
p.conf.RunOnConnect, p.conf.RunOnConnect,
p.conf.RunOnConnectRestart, p.conf.RunOnConnectRestart,
p.externalCmdPool,
p.metrics, p.metrics,
p.pathManager, p.pathManager,
p) p)
@ -331,6 +334,7 @@ func (p *Core) createResources(initial bool) error {
p.hlsServer, err = newHLSServer( p.hlsServer, err = newHLSServer(
p.ctx, p.ctx,
p.conf.HLSAddress, p.conf.HLSAddress,
p.conf.ExternalAuthenticationURL,
p.conf.HLSAlwaysRemux, p.conf.HLSAlwaysRemux,
p.conf.HLSSegmentCount, p.conf.HLSSegmentCount,
p.conf.HLSSegmentDuration, p.conf.HLSSegmentDuration,
@ -411,6 +415,7 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) {
if newConf == nil || if 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 ||
@ -435,6 +440,7 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) {
if newConf == nil || if 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 ||
@ -455,6 +461,7 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) {
if newConf == nil || if newConf == nil ||
newConf.RTMPDisable != p.conf.RTMPDisable || newConf.RTMPDisable != p.conf.RTMPDisable ||
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 ||
@ -470,6 +477,7 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) {
if newConf == nil || if newConf == nil ||
newConf.HLSDisable != p.conf.HLSDisable || newConf.HLSDisable != p.conf.HLSDisable ||
newConf.HLSAddress != p.conf.HLSAddress || newConf.HLSAddress != p.conf.HLSAddress ||
newConf.ExternalAuthenticationURL != p.conf.ExternalAuthenticationURL ||
newConf.HLSAlwaysRemux != p.conf.HLSAlwaysRemux || newConf.HLSAlwaysRemux != p.conf.HLSAlwaysRemux ||
newConf.HLSSegmentCount != p.conf.HLSSegmentCount || newConf.HLSSegmentCount != p.conf.HLSSegmentCount ||
newConf.HLSSegmentDuration != p.conf.HLSSegmentDuration || newConf.HLSSegmentDuration != p.conf.HLSSegmentDuration ||

42
internal/core/externalauth.go

@ -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
}

76
internal/core/hls_muxer.go

@ -123,6 +123,7 @@ type hlsMuxerParent interface {
type hlsMuxer struct { type hlsMuxer struct {
name string name string
externalAuthenticationURL string
hlsAlwaysRemux bool hlsAlwaysRemux bool
hlsSegmentCount int hlsSegmentCount int
hlsSegmentDuration conf.StringDuration hlsSegmentDuration conf.StringDuration
@ -148,6 +149,7 @@ type hlsMuxer struct {
func newHLSMuxer( func newHLSMuxer(
parentCtx context.Context, parentCtx context.Context,
name string, name string,
externalAuthenticationURL string,
hlsAlwaysRemux bool, hlsAlwaysRemux bool,
hlsSegmentCount int, hlsSegmentCount int,
hlsSegmentDuration conf.StringDuration, hlsSegmentDuration conf.StringDuration,
@ -160,6 +162,7 @@ func newHLSMuxer(
m := &hlsMuxer{ m := &hlsMuxer{
name: name, name: name,
externalAuthenticationURL: externalAuthenticationURL,
hlsAlwaysRemux: hlsAlwaysRemux, hlsAlwaysRemux: hlsAlwaysRemux,
hlsSegmentCount: hlsSegmentCount, hlsSegmentCount: hlsSegmentCount,
hlsSegmentDuration: hlsSegmentDuration, hlsSegmentDuration: hlsSegmentDuration,
@ -261,8 +264,7 @@ func (m *hlsMuxer) runInner(innerCtx context.Context, innerReady chan struct{})
res := m.pathManager.onReaderSetupPlay(pathReaderSetupPlayReq{ res := m.pathManager.onReaderSetupPlay(pathReaderSetupPlayReq{
Author: m, Author: m,
PathName: m.pathName, PathName: m.pathName,
IP: nil, Authenticate: nil,
ValidateCredentials: nil,
}) })
if res.Err != nil { if res.Err != nil {
return res.Err return res.Err
@ -413,20 +415,15 @@ func (m *hlsMuxer) runInner(innerCtx context.Context, innerReady chan struct{})
func (m *hlsMuxer) handleRequest(req hlsMuxerRequest) hlsMuxerResponse { func (m *hlsMuxer) handleRequest(req hlsMuxerRequest) hlsMuxerResponse {
atomic.StoreInt64(m.lastRequestTime, time.Now().Unix()) atomic.StoreInt64(m.lastRequestTime, time.Now().Unix())
conf := m.path.Conf() err := m.authenticate(req.Req)
if err != nil {
if conf.ReadIPs != nil { if terr, ok := err.(pathErrAuthCritical); ok {
tmp, _, _ := net.SplitHostPort(req.Req.RemoteAddr) m.log(logger.Info, "authentication error: %s", terr.Message)
ip := net.ParseIP(tmp) return hlsMuxerResponse{
if !ipEqualOrInRange(ip, conf.ReadIPs) { Status: http.StatusUnauthorized,
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) {
return hlsMuxerResponse{ return hlsMuxerResponse{
Status: http.StatusUnauthorized, Status: http.StatusUnauthorized,
Header: map[string]string{ Header: map[string]string{
@ -434,7 +431,6 @@ func (m *hlsMuxer) handleRequest(req hlsMuxerRequest) hlsMuxerResponse {
}, },
} }
} }
}
switch { switch {
case req.File == "index.m3u8": case req.File == "index.m3u8":
@ -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). // onRequest is called by hlsserver.Server (forwarded from ServeHTTP).
func (m *hlsMuxer) onRequest(req hlsMuxerRequest) { func (m *hlsMuxer) onRequest(req hlsMuxerRequest) {
select { select {

4
internal/core/hls_server.go

@ -45,6 +45,7 @@ type hlsServerParent interface {
} }
type hlsServer struct { type hlsServer struct {
externalAuthenticationURL string
hlsAlwaysRemux bool hlsAlwaysRemux bool
hlsSegmentCount int hlsSegmentCount int
hlsSegmentDuration conf.StringDuration hlsSegmentDuration conf.StringDuration
@ -70,6 +71,7 @@ type hlsServer struct {
func newHLSServer( func newHLSServer(
parentCtx context.Context, parentCtx context.Context,
address string, address string,
externalAuthenticationURL string,
hlsAlwaysRemux bool, hlsAlwaysRemux bool,
hlsSegmentCount int, hlsSegmentCount int,
hlsSegmentDuration conf.StringDuration, hlsSegmentDuration conf.StringDuration,
@ -87,6 +89,7 @@ func newHLSServer(
ctx, ctxCancel := context.WithCancel(parentCtx) ctx, ctxCancel := context.WithCancel(parentCtx)
s := &hlsServer{ s := &hlsServer{
externalAuthenticationURL: externalAuthenticationURL,
hlsAlwaysRemux: hlsAlwaysRemux, hlsAlwaysRemux: hlsAlwaysRemux,
hlsSegmentCount: hlsSegmentCount, hlsSegmentCount: hlsSegmentCount,
hlsSegmentDuration: hlsSegmentDuration, hlsSegmentDuration: hlsSegmentDuration,
@ -268,6 +271,7 @@ func (s *hlsServer) findOrCreateMuxer(pathName string) *hlsMuxer {
r = newHLSMuxer( r = newHLSMuxer(
s.ctx, s.ctx,
pathName, pathName,
s.externalAuthenticationURL,
s.hlsAlwaysRemux, s.hlsAlwaysRemux,
s.hlsSegmentCount, s.hlsSegmentCount,
s.hlsSegmentDuration, s.hlsSegmentDuration,

134
internal/core/hls_server_test.go

@ -1,13 +1,77 @@
package core package core
import ( import (
"context"
"encoding/json"
"net"
"net/http" "net/http"
"testing" "testing"
"time" "time"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/require" "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) { func TestHLSServerNotFound(t *testing.T) {
p, ok := newInstance("") p, ok := newInstance("")
require.Equal(t, true, ok) require.Equal(t, true, ok)
@ -52,36 +116,78 @@ func TestHLSServerRead(t *testing.T) {
require.Equal(t, 0, cnt2.wait()) require.Equal(t, 0, cnt2.wait())
} }
func TestHLSServerReadAuth(t *testing.T) { func TestHLSServerAuth(t *testing.T) {
p, ok := newInstance( for _, mode := range []string{
"paths:\n" + "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" + " all:\n" +
" readUser: testuser\n" + " readUser: testreader\n" +
" readPass: testpass\n" + " readPass: testpass\n" +
" readIPs: [127.0.0.0/16]\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) require.Equal(t, true, ok)
defer p.close() defer p.close()
var a *testHTTPAuthenticator
if mode == "external" {
var err error
a, err = newTestHTTPAuthenticator("publish")
require.NoError(t, err)
}
cnt1, err := newContainer("ffmpeg", "source", []string{ cnt1, err := newContainer("ffmpeg", "source", []string{
"-re", "-re",
"-stream_loop", "-1", "-stream_loop", "-1",
"-i", "emptyvideo.mkv", "-i", "emptyvideo.mkv",
"-c", "copy", "-c", "copy",
"-f", "rtsp", "-f", "rtsp",
"rtsp://localhost:8554/teststream", "rtsp://testpublisher:testpass@localhost:8554/teststream",
}) })
require.NoError(t, err) require.NoError(t, err)
defer cnt1.close() defer cnt1.close()
time.Sleep(1 * time.Second) time.Sleep(1 * time.Second)
cnt2, err := newContainer("ffmpeg", "dest", []string{ if mode == "external" {
"-i", "http://testuser:testpass@127.0.0.1:8888/teststream/index.m3u8", a.close()
"-vframes", "1", var err error
"-f", "image2", a, err = newTestHTTPAuthenticator("read")
"-y", "/dev/null",
})
require.NoError(t, err) require.NoError(t, err)
defer cnt2.close() defer a.close()
require.Equal(t, 0, cnt2.wait()) }
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)
}
})
}
}
} }

21
internal/core/path.go

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

120
internal/core/path_manager.go

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

58
internal/core/rtmp_conn.go

@ -53,8 +53,8 @@ type rtmpConnParent interface {
} }
type rtmpConn struct { type rtmpConn struct {
externalCmdPool *externalcmd.Pool
id string id string
externalAuthenticationURL string
rtspAddress string rtspAddress string
readTimeout conf.StringDuration readTimeout conf.StringDuration
writeTimeout conf.StringDuration writeTimeout conf.StringDuration
@ -63,6 +63,7 @@ type rtmpConn struct {
runOnConnectRestart bool runOnConnectRestart bool
wg *sync.WaitGroup wg *sync.WaitGroup
conn *rtmp.Conn conn *rtmp.Conn
externalCmdPool *externalcmd.Pool
pathManager rtmpConnPathManager pathManager rtmpConnPathManager
parent rtmpConnParent parent rtmpConnParent
@ -76,8 +77,8 @@ type rtmpConn struct {
func newRTMPConn( func newRTMPConn(
parentCtx context.Context, parentCtx context.Context,
externalCmdPool *externalcmd.Pool,
id string, id string,
externalAuthenticationURL string,
rtspAddress string, rtspAddress string,
readTimeout conf.StringDuration, readTimeout conf.StringDuration,
writeTimeout conf.StringDuration, writeTimeout conf.StringDuration,
@ -86,13 +87,14 @@ func newRTMPConn(
runOnConnectRestart bool, runOnConnectRestart bool,
wg *sync.WaitGroup, wg *sync.WaitGroup,
nconn net.Conn, nconn net.Conn,
externalCmdPool *externalcmd.Pool,
pathManager rtmpConnPathManager, pathManager rtmpConnPathManager,
parent rtmpConnParent) *rtmpConn { parent rtmpConnParent) *rtmpConn {
ctx, ctxCancel := context.WithCancel(parentCtx) ctx, ctxCancel := context.WithCancel(parentCtx)
c := &rtmpConn{ c := &rtmpConn{
externalCmdPool: externalCmdPool,
id: id, id: id,
externalAuthenticationURL: externalAuthenticationURL,
rtspAddress: rtspAddress, rtspAddress: rtspAddress,
readTimeout: readTimeout, readTimeout: readTimeout,
writeTimeout: writeTimeout, writeTimeout: writeTimeout,
@ -101,6 +103,7 @@ func newRTMPConn(
runOnConnectRestart: runOnConnectRestart, runOnConnectRestart: runOnConnectRestart,
wg: wg, wg: wg,
conn: rtmp.NewServerConn(nconn), conn: rtmp.NewServerConn(nconn),
externalCmdPool: externalCmdPool,
pathManager: pathManager, pathManager: pathManager,
parent: parent, parent: parent,
ctx: ctx, ctx: ctx,
@ -219,9 +222,11 @@ func (c *rtmpConn) runRead(ctx context.Context) error {
res := c.pathManager.onReaderSetupPlay(pathReaderSetupPlayReq{ res := c.pathManager.onReaderSetupPlay(pathReaderSetupPlayReq{
Author: c, Author: c,
PathName: pathName, PathName: pathName,
IP: c.ip(), Authenticate: func(
ValidateCredentials: func(pathUser conf.Credential, pathPass conf.Credential) error { pathIPs []interface{},
return c.validateCredentials(pathUser, pathPass, query) 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 {
res := c.pathManager.onPublisherAnnounce(pathPublisherAnnounceReq{ res := c.pathManager.onPublisherAnnounce(pathPublisherAnnounceReq{
Author: c, Author: c,
PathName: pathName, PathName: pathName,
IP: c.ip(), Authenticate: func(
ValidateCredentials: func(pathUser conf.Credential, pathPass conf.Credential) error { pathIPs []interface{},
return c.validateCredentials(pathUser, pathPass, query) 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 {
} }
} }
func (c *rtmpConn) validateCredentials( func (c *rtmpConn) authenticate(
pathName string,
pathIPs []interface{},
pathUser conf.Credential, pathUser conf.Credential,
pathPass conf.Credential, pathPass conf.Credential,
action string,
query url.Values, query url.Values,
) error { ) error {
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) || if query.Get("user") != string(pathUser) ||
query.Get("pass") != string(pathPass) { query.Get("pass") != string(pathPass) {
return pathErrAuthCritical{ return pathErrAuthCritical{
Message: "wrong username or password", Message: "invalid credentials",
}
} }
} }

12
internal/core/rtmp_server.go

@ -48,13 +48,14 @@ type rtmpServerParent interface {
} }
type rtmpServer struct { type rtmpServer struct {
externalCmdPool *externalcmd.Pool externalAuthenticationURL string
readTimeout conf.StringDuration readTimeout conf.StringDuration
writeTimeout conf.StringDuration writeTimeout conf.StringDuration
readBufferCount int readBufferCount int
rtspAddress string rtspAddress string
runOnConnect string runOnConnect string
runOnConnectRestart bool runOnConnectRestart bool
externalCmdPool *externalcmd.Pool
metrics *metrics metrics *metrics
pathManager *pathManager pathManager *pathManager
parent rtmpServerParent parent rtmpServerParent
@ -73,7 +74,7 @@ type rtmpServer struct {
func newRTMPServer( func newRTMPServer(
parentCtx context.Context, parentCtx context.Context,
externalCmdPool *externalcmd.Pool, externalAuthenticationURL string,
address string, address string,
readTimeout conf.StringDuration, readTimeout conf.StringDuration,
writeTimeout conf.StringDuration, writeTimeout conf.StringDuration,
@ -81,6 +82,7 @@ func newRTMPServer(
rtspAddress string, rtspAddress string,
runOnConnect string, runOnConnect string,
runOnConnectRestart bool, runOnConnectRestart bool,
externalCmdPool *externalcmd.Pool,
metrics *metrics, metrics *metrics,
pathManager *pathManager, pathManager *pathManager,
parent rtmpServerParent) (*rtmpServer, error) { parent rtmpServerParent) (*rtmpServer, error) {
@ -92,13 +94,14 @@ func newRTMPServer(
ctx, ctxCancel := context.WithCancel(parentCtx) ctx, ctxCancel := context.WithCancel(parentCtx)
s := &rtmpServer{ s := &rtmpServer{
externalCmdPool: externalCmdPool, 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,
externalCmdPool: externalCmdPool,
metrics: metrics, metrics: metrics,
pathManager: pathManager, pathManager: pathManager,
parent: parent, parent: parent,
@ -174,8 +177,8 @@ outer:
c := newRTMPConn( c := newRTMPConn(
s.ctx, s.ctx,
s.externalCmdPool,
id, id,
s.externalAuthenticationURL,
s.rtspAddress, s.rtspAddress,
s.readTimeout, s.readTimeout,
s.writeTimeout, s.writeTimeout,
@ -184,6 +187,7 @@ outer:
s.runOnConnectRestart, s.runOnConnectRestart,
&s.wg, &s.wg,
nconn, nconn,
s.externalCmdPool,
s.pathManager, s.pathManager,
s) s)
s.conns[c] = struct{}{} s.conns[c] = struct{}{}

131
internal/core/rtmp_server_test.go

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

1
internal/core/rtmp_source.go

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

94
internal/core/rtsp_conn.go

@ -2,6 +2,7 @@ package core
import ( import (
"errors" "errors"
"fmt"
"net" "net"
"time" "time"
@ -24,12 +25,13 @@ type rtspConnParent interface {
} }
type rtspConn struct { type rtspConn struct {
externalCmdPool *externalcmd.Pool 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
pathManager *pathManager pathManager *pathManager
conn *gortsplib.ServerConn conn *gortsplib.ServerConn
parent rtspConnParent parent rtspConnParent
@ -42,22 +44,24 @@ type rtspConn struct {
} }
func newRTSPConn( func newRTSPConn(
externalCmdPool *externalcmd.Pool, 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,
pathManager *pathManager, pathManager *pathManager,
conn *gortsplib.ServerConn, conn *gortsplib.ServerConn,
parent rtspConnParent) *rtspConn { parent rtspConnParent) *rtspConn {
c := &rtspConn{ c := &rtspConn{
externalCmdPool: externalCmdPool, externalAuthenticationURL: externalAuthenticationURL,
rtspAddress: rtspAddress, rtspAddress: rtspAddress,
authMethods: authMethods, authMethods: authMethods,
readTimeout: readTimeout, readTimeout: readTimeout,
runOnConnect: runOnConnect, runOnConnect: runOnConnect,
runOnConnectRestart: runOnConnectRestart, runOnConnectRestart: runOnConnectRestart,
externalCmdPool: externalCmdPool,
pathManager: pathManager, pathManager: pathManager,
conn: conn, conn: conn,
parent: parent, parent: parent,
@ -97,11 +101,78 @@ 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) validateCredentials( func (c *rtspConn) authenticate(
pathName string,
pathIPs []interface{},
pathUser conf.Credential, pathUser conf.Credential,
pathPass conf.Credential, pathPass conf.Credential,
action string,
req *base.Request, req *base.Request,
) error { ) error {
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(),
},
},
}
}
}
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 // reset authValidator every time the credentials change
if c.authValidator == nil || c.authUser != string(pathUser) || c.authPass != string(pathPass) { if c.authValidator == nil || c.authUser != string(pathUser) || c.authPass != string(pathPass) {
c.authUser = string(pathUser) c.authUser = string(pathUser)
@ -113,7 +184,7 @@ func (c *rtspConn) validateCredentials(
if err != nil { if err != nil {
c.authFailures++ c.authFailures++
// vlc with login prompt sends 4 requests: // VLC with login prompt sends 4 requests:
// 1) without credentials // 1) without credentials
// 2) with password but without username // 2) with password but without username
// 3) without credentials // 3) without credentials
@ -128,10 +199,6 @@ func (c *rtspConn) validateCredentials(
} }
} }
if c.authFailures > 1 {
c.log(logger.Debug, "WARN: unauthorized: %s", err)
}
return pathErrAuthNotCritical{ return pathErrAuthNotCritical{
Response: &base.Response{ Response: &base.Response{
StatusCode: base.StatusUnauthorized, StatusCode: base.StatusUnauthorized,
@ -144,6 +211,7 @@ func (c *rtspConn) validateCredentials(
// login successful, reset authFailures // login successful, reset authFailures
c.authFailures = 0 c.authFailures = 0
}
return nil return nil
} }
@ -174,9 +242,11 @@ func (c *rtspConn) onDescribe(ctx *gortsplib.ServerHandlerOnDescribeCtx,
res := c.pathManager.onDescribe(pathDescribeReq{ res := c.pathManager.onDescribe(pathDescribeReq{
PathName: ctx.Path, PathName: ctx.Path,
URL: ctx.Req.URL, URL: ctx.Req.URL,
IP: c.ip(), Authenticate: func(
ValidateCredentials: func(pathUser conf.Credential, pathPass conf.Credential) error { pathIPs []interface{},
return c.validateCredentials(pathUser, pathPass, ctx.Req) pathUser conf.Credential,
pathPass conf.Credential) error {
return c.authenticate(ctx.Path, pathIPs, pathUser, pathPass, "read", ctx.Req)
}, },
}) })

14
internal/core/rtsp_server.go

@ -50,7 +50,7 @@ type rtspServerParent interface {
} }
type rtspServer struct { type rtspServer struct {
externalCmdPool *externalcmd.Pool externalAuthenticationURL string
authMethods []headers.AuthMethod authMethods []headers.AuthMethod
readTimeout conf.StringDuration readTimeout conf.StringDuration
isTLS bool isTLS bool
@ -58,6 +58,7 @@ type rtspServer struct {
protocols map[conf.Protocol]struct{} protocols map[conf.Protocol]struct{}
runOnConnect string runOnConnect string
runOnConnectRestart bool runOnConnectRestart bool
externalCmdPool *externalcmd.Pool
metrics *metrics metrics *metrics
pathManager *pathManager pathManager *pathManager
parent rtspServerParent parent rtspServerParent
@ -73,7 +74,7 @@ type rtspServer struct {
func newRTSPServer( func newRTSPServer(
parentCtx context.Context, parentCtx context.Context,
externalCmdPool *externalcmd.Pool, externalAuthenticationURL string,
address string, address string,
authMethods []headers.AuthMethod, authMethods []headers.AuthMethod,
readTimeout conf.StringDuration, readTimeout conf.StringDuration,
@ -94,18 +95,20 @@ func newRTSPServer(
protocols map[conf.Protocol]struct{}, protocols map[conf.Protocol]struct{},
runOnConnect string, runOnConnect string,
runOnConnectRestart bool, runOnConnectRestart bool,
externalCmdPool *externalcmd.Pool,
metrics *metrics, metrics *metrics,
pathManager *pathManager, pathManager *pathManager,
parent rtspServerParent) (*rtspServer, error) { parent rtspServerParent) (*rtspServer, error) {
ctx, ctxCancel := context.WithCancel(parentCtx) ctx, ctxCancel := context.WithCancel(parentCtx)
s := &rtspServer{ s := &rtspServer{
externalCmdPool: externalCmdPool, externalAuthenticationURL: externalAuthenticationURL,
authMethods: authMethods, authMethods: authMethods,
readTimeout: readTimeout, readTimeout: readTimeout,
isTLS: isTLS, isTLS: isTLS,
rtspAddress: rtspAddress, rtspAddress: rtspAddress,
protocols: protocols, protocols: protocols,
externalCmdPool: externalCmdPool,
metrics: metrics, metrics: metrics,
pathManager: pathManager, pathManager: pathManager,
parent: parent, parent: parent,
@ -255,12 +258,13 @@ func (s *rtspServer) newSessionID() (string, error) {
// 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.externalCmdPool, s.externalAuthenticationURL,
s.rtspAddress, s.rtspAddress,
s.authMethods, s.authMethods,
s.readTimeout, s.readTimeout,
s.runOnConnect, s.runOnConnect,
s.runOnConnectRestart, s.runOnConnectRestart,
s.externalCmdPool,
s.pathManager, s.pathManager,
ctx.Conn, ctx.Conn,
s) s)
@ -305,12 +309,12 @@ func (s *rtspServer) OnSessionOpen(ctx *gortsplib.ServerHandlerOnSessionOpenCtx)
id, _ := s.newSessionID() id, _ := s.newSessionID()
se := newRTSPSession( se := newRTSPSession(
s.externalCmdPool,
s.isTLS, s.isTLS,
s.protocols, s.protocols,
id, id,
ctx.Session, ctx.Session,
ctx.Conn, ctx.Conn,
s.externalCmdPool,
s.pathManager, s.pathManager,
s) s)

113
internal/core/rtsp_server_test.go

@ -195,17 +195,40 @@ func TestRTSPServerPublishRead(t *testing.T) {
} }
func TestRTSPServerAuth(t *testing.T) { func TestRTSPServerAuth(t *testing.T) {
t.Run("publish", func(t *testing.T) { for _, ca := range []string{
p, ok := newInstance("rtmpDisable: yes\n" + "internal",
"external",
} {
t.Run(ca, func(t *testing.T) {
var conf string
if ca == "internal" {
conf = "rtmpDisable: yes\n" +
"hlsDisable: yes\n" + "hlsDisable: yes\n" +
"paths:\n" + "paths:\n" +
" all:\n" + " all:\n" +
" publishUser: testuser\n" + " publishUser: testpublisher\n" +
" publishPass: test!$()*+.;<=>[]^_-{}\n" + " publishPass: testpass\n" +
" publishIPs: [127.0.0.0/16]\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) require.Equal(t, true, ok)
defer p.close() defer p.close()
var a *testHTTPAuthenticator
if ca == "external" {
var err error
a, err = newTestHTTPAuthenticator("publish")
require.NoError(t, err)
}
track, err := gortsplib.NewTrackH264(96, track, err := gortsplib.NewTrackH264(96,
&gortsplib.TrackConfigH264{SPS: []byte{0x01, 0x02, 0x03, 0x04}, PPS: []byte{0x01, 0x02, 0x03, 0x04}}) &gortsplib.TrackConfigH264{SPS: []byte{0x01, 0x02, 0x03, 0x04}, PPS: []byte{0x01, 0x02, 0x03, 0x04}})
require.NoError(t, err) require.NoError(t, err)
@ -213,60 +236,24 @@ func TestRTSPServerAuth(t *testing.T) {
source := gortsplib.Client{} source := gortsplib.Client{}
err = source.StartPublishing( 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", "rtsp://testpublisher:testpass@127.0.0.1:8554/teststream",
gortsplib.Tracks{track}) gortsplib.Tracks{track})
require.NoError(t, err) require.NoError(t, err)
defer source.Close() defer source.Close()
})
for _, soft := range []string{ if ca == "external" {
"ffmpeg", a.close()
"vlc", var err error
} { a, err = newTestHTTPAuthenticator("read")
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")
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",
})
require.NoError(t, err) require.NoError(t, err)
defer cnt1.close() defer a.close()
}
time.Sleep(1 * time.Second) reader := gortsplib.Client{}
if soft == "ffmpeg" { err = reader.StartReading("rtsp://testreader:testpass@127.0.0.1:8554/teststream")
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) require.NoError(t, err)
defer cnt2.close() defer reader.Close()
require.Equal(t, 0, cnt2.wait())
} else {
cnt2, err := newContainer("vlc", "dest", []string{
"rtsp://testuser:test!$()*+.;<=>[]^_-{}@localhost:8554/test/stream",
})
require.NoError(t, err)
defer cnt2.close()
require.Equal(t, 0, cnt2.wait())
}
}) })
} }
@ -401,6 +388,30 @@ func TestRTSPServerAuthFail(t *testing.T) {
) )
require.EqualError(t, err, "bad status code: 401 (Unauthorized)") 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) { func TestRTSPServerPublisherOverride(t *testing.T) {

22
internal/core/rtsp_session.go

@ -29,12 +29,12 @@ type rtspSessionParent interface {
} }
type rtspSession struct { type rtspSession struct {
externalCmdPool *externalcmd.Pool
isTLS bool isTLS bool
protocols map[conf.Protocol]struct{} protocols map[conf.Protocol]struct{}
id string id string
ss *gortsplib.ServerSession ss *gortsplib.ServerSession
author *gortsplib.ServerConn author *gortsplib.ServerConn
externalCmdPool *externalcmd.Pool
pathManager rtspSessionPathManager pathManager rtspSessionPathManager
parent rtspSessionParent parent rtspSessionParent
@ -48,21 +48,21 @@ type rtspSession struct {
} }
func newRTSPSession( func newRTSPSession(
externalCmdPool *externalcmd.Pool,
isTLS bool, isTLS bool,
protocols map[conf.Protocol]struct{}, protocols map[conf.Protocol]struct{},
id string, id string,
ss *gortsplib.ServerSession, ss *gortsplib.ServerSession,
sc *gortsplib.ServerConn, sc *gortsplib.ServerConn,
externalCmdPool *externalcmd.Pool,
pathManager rtspSessionPathManager, pathManager rtspSessionPathManager,
parent rtspSessionParent) *rtspSession { parent rtspSessionParent) *rtspSession {
s := &rtspSession{ s := &rtspSession{
externalCmdPool: externalCmdPool,
isTLS: isTLS, isTLS: isTLS,
protocols: protocols, protocols: protocols,
id: id, id: id,
ss: ss, ss: ss,
author: sc, author: sc,
externalCmdPool: externalCmdPool,
pathManager: pathManager, pathManager: pathManager,
parent: parent, parent: parent,
} }
@ -157,9 +157,11 @@ func (s *rtspSession) onAnnounce(c *rtspConn, ctx *gortsplib.ServerHandlerOnAnno
res := s.pathManager.onPublisherAnnounce(pathPublisherAnnounceReq{ res := s.pathManager.onPublisherAnnounce(pathPublisherAnnounceReq{
Author: s, Author: s,
PathName: ctx.Path, PathName: ctx.Path,
IP: ctx.Conn.NetConn().RemoteAddr().(*net.TCPAddr).IP, Authenticate: func(
ValidateCredentials: func(pathUser conf.Credential, pathPass conf.Credential) error { pathIPs []interface{},
return c.validateCredentials(pathUser, pathPass, ctx.Req) 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
res := s.pathManager.onReaderSetupPlay(pathReaderSetupPlayReq{ res := s.pathManager.onReaderSetupPlay(pathReaderSetupPlayReq{
Author: s, Author: s,
PathName: ctx.Path, PathName: ctx.Path,
IP: ctx.Conn.NetConn().RemoteAddr().(*net.TCPAddr).IP, Authenticate: func(
ValidateCredentials: func(pathUser conf.Credential, pathPass conf.Credential) error { pathIPs []interface{},
return c.validateCredentials(pathUser, pathPass, ctx.Req) 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
# A higher number allows a wider throughput, a lower number allows to save RAM. # A higher number allows a wider throughput, a lower number allows to save RAM.
readBufferCount: 512 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. # Enable the HTTP API.
api: no api: no
# Address of the API listener. # Address of the API listener.

Loading…
Cancel
Save