support hashed credentials

This commit is contained in:
aler9 2020-12-31 19:47:25 +01:00
parent 36caa51081
commit 3050893d32
7 changed files with 100 additions and 49 deletions

View File

@ -29,6 +29,7 @@ Features:
* [Advanced usage and FAQs](#advanced-usage-and-faqs)
* [Configuration](#configuration)
* [Encryption](#encryption)
* [Authentication](#authentication)
* [RTSP proxy mode](#rtsp-proxy-mode)
* [Publish a webcam](#publish-a-webcam)
* [Publish a Raspberry Pi Camera](#publish-a-raspberry-pi-camera)
@ -37,7 +38,6 @@ Features:
* [On-demand publishing](#on-demand-publishing)
* [Redirect to another server](#redirect-to-another-server)
* [Fallback stream](#fallback-stream)
* [Authentication](#authentication)
* [Start on boot with systemd](#start-on-boot-with-systemd)
* [Monitoring](#monitoring)
* [Command-line usage](#command-line-usage)
@ -167,6 +167,52 @@ gst-launch-1.0 rtspsrc location=rtsps://ip:8555/... tls-validation-flags=0
If the client is _VLC_, encryption can't be deployed, since _VLC_ doesn't support it.
### Authentication
Edit `rtsp-simple-server.yml` and replace everything inside section `paths` with the following content:
```yml
paths:
all:
publishUser: admin
publishPass: mypassword
```
Only publishers that provide both username and password will be able to proceed:
```
ffmpeg -re -stream_loop -1 -i file.ts -c copy -f rtsp rtsp://admin:mypassword@localhost:8554/mystream
```
It's possible to setup authentication for readers too:
```yml
paths:
all:
publishUser: admin
publishPass: mypass
readUser: user
readPass: userpass
```
If storing plain credentials in the configuration file is a security problem, username and passwords can be stored as sha256-hashed values; a value must be converted into sha256:
```
echo -n "userpass" | openssl dgst -binary -sha256 | openssl base64
```
Then stored with the `sha256:` prefix:
```yml
paths:
all:
readUser: sha256:j1tsRqDEw9xvq/D7/9tMx6Jh/jMhk3UfjwIB2f1zgMo=
readPass: sha256:BdSWkrdV+ZxFBLUQQY7+7uv9RmiSVA8nrPmjGjJtZQQ=
```
**WARNING**: enable encryption or use a VPN to ensure that no one is intercepting and reading the credentials.
### RTSP proxy mode
_rtsp-simple-server_ is also a RTSP proxy, that is usually deployed in one of these scenarios:
@ -321,37 +367,6 @@ paths:
fallback: rtsp://otherurl/otherpath
```
### Authentication
Edit `rtsp-simple-server.yml` and replace everything inside section `paths` with the following content:
```yml
paths:
all:
publishUser: admin
publishPass: mypassword
```
Only publishers that provide both username and password will be able to proceed:
```
ffmpeg -re -stream_loop -1 -i file.ts -c copy -f rtsp rtsp://admin:mypassword@localhost:8554/mystream
```
It's possible to setup authentication for readers too:
```yml
paths:
all:
publishUser: admin
publishPass: mypassword
readUser: user
readPass: userpassword
```
**WARNING**: enable encryption or use a VPN to ensure that no one is intercepting and reading the credentials.
### Start on boot with systemd
Systemd is the service manager used by Ubuntu, Debian and many other Linux distributions, and allows to launch rtsp-simple-server on boot.

2
go.mod
View File

@ -5,7 +5,7 @@ go 1.15
require (
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d // indirect
github.com/aler9/gortsplib v0.0.0-20201217115908-9a602f77f77c
github.com/aler9/gortsplib v0.0.0-20201231182741-9bd587e576f1
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fsnotify/fsnotify v1.4.9
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51

4
go.sum
View File

@ -2,8 +2,8 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafo
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d h1:UQZhZ2O0vMHr2cI+DC1Mbh0TJxzA3RcLoMsFw+aXw7E=
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
github.com/aler9/gortsplib v0.0.0-20201217115908-9a602f77f77c h1:Qbb+ccjoEoPPCJLofGtdvWwxzCS4v+vogK43Om9D3OM=
github.com/aler9/gortsplib v0.0.0-20201217115908-9a602f77f77c/go.mod h1:8P09VjpiPJFyfkVosyF5/TY82jNwkMN165NS/7sc32I=
github.com/aler9/gortsplib v0.0.0-20201231182741-9bd587e576f1 h1:RqOBalGfTyA43DigWv64uKNEiAIJT9IPiXrjkDbR/Lo=
github.com/aler9/gortsplib v0.0.0-20201231182741-9bd587e576f1/go.mod h1:8P09VjpiPJFyfkVosyF5/TY82jNwkMN165NS/7sc32I=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=

View File

@ -107,7 +107,7 @@ type Client struct {
path Path
authUser string
authPass string
authHelper *auth.Server
authValidator *auth.Validator
authFailures int
streamProtocol gortsplib.StreamProtocol
streamTracks map[int]*streamTrack
@ -896,14 +896,14 @@ func (c *Client) Authenticate(authMethods []headers.AuthMethod, ips []interface{
// validate user
if user != "" {
// reset authHelper every time the credentials change
if c.authHelper == nil || c.authUser != user || c.authPass != pass {
// reset authValidator every time the credentials change
if c.authValidator == nil || c.authUser != user || c.authPass != pass {
c.authUser = user
c.authPass = pass
c.authHelper = auth.NewServer(user, pass, authMethods)
c.authValidator = auth.NewValidator(user, pass, authMethods)
}
err := c.authHelper.ValidateHeader(req.Header["Authorization"], req.Method, req.URL)
err := c.authValidator.ValidateHeader(req.Header["Authorization"], req.Method, req.URL)
if err != nil {
c.authFailures++
@ -919,7 +919,7 @@ func (c *Client) Authenticate(authMethods []headers.AuthMethod, ips []interface{
return errAuthCritical{&base.Response{
StatusCode: base.StatusUnauthorized,
Header: base.Header{
"WWW-Authenticate": c.authHelper.GenerateHeader(),
"WWW-Authenticate": c.authValidator.GenerateHeader(),
},
}}
}
@ -931,7 +931,7 @@ func (c *Client) Authenticate(authMethods []headers.AuthMethod, ips []interface{
return errAuthNotCritical{&base.Response{
StatusCode: base.StatusUnauthorized,
Header: base.Header{
"WWW-Authenticate": c.authHelper.GenerateHeader(),
"WWW-Authenticate": c.authValidator.GenerateHeader(),
},
}}
}

View File

@ -165,17 +165,15 @@ func (pconf *PathConf) fillAndCheck(name string) error {
}
if pconf.PublishUser != "" {
if !reUserPass.MatchString(pconf.PublishUser) {
if !strings.HasPrefix(pconf.PublishUser, "sha256:") && !reUserPass.MatchString(pconf.PublishUser) {
return fmt.Errorf("publish username contains unsupported characters (supported are %s)", userPassSupportedChars)
}
}
if pconf.PublishPass != "" {
if !reUserPass.MatchString(pconf.PublishPass) {
if !strings.HasPrefix(pconf.PublishPass, "sha256:") && !reUserPass.MatchString(pconf.PublishPass) {
return fmt.Errorf("publish password contains unsupported characters (supported are %s)", userPassSupportedChars)
}
}
if len(pconf.PublishIps) > 0 {
var err error
pconf.PublishIpsParsed, err = parseIPCidrList(pconf.PublishIps)
@ -191,19 +189,18 @@ func (pconf *PathConf) fillAndCheck(name string) error {
return fmt.Errorf("read username and password must be both filled")
}
if pconf.ReadUser != "" {
if !reUserPass.MatchString(pconf.ReadUser) {
if !strings.HasPrefix(pconf.ReadUser, "sha256:") && !reUserPass.MatchString(pconf.ReadUser) {
return fmt.Errorf("read username contains unsupported characters (supported are %s)", userPassSupportedChars)
}
}
if pconf.ReadPass != "" {
if !reUserPass.MatchString(pconf.ReadPass) {
if !strings.HasPrefix(pconf.ReadPass, "sha256:") && !reUserPass.MatchString(pconf.ReadPass) {
return fmt.Errorf("read password contains unsupported characters (supported are %s)", userPassSupportedChars)
}
}
if pconf.ReadUser != "" && pconf.ReadPass == "" || pconf.ReadUser == "" && pconf.ReadPass != "" {
return fmt.Errorf("read username and password must be both filled")
}
if len(pconf.ReadIps) > 0 {
var err error
pconf.ReadIpsParsed, err = parseIPCidrList(pconf.ReadIps)

View File

@ -619,6 +619,41 @@ func TestAuth(t *testing.T) {
}
}
func TestAuthHashed(t *testing.T) {
p, ok := testProgram("paths:\n" +
" all:\n" +
" readUser: sha256:rl3rgi4NcZkpAEcacZnQ2VuOfJ0FxAqCRaKB/SwdZoQ=\n" +
" readPass: sha256:E9JJ8stBJ7QM+nV4ZoUCeHk/gU3tPFh/5YieiJp6n2w=\n")
require.Equal(t, true, ok)
defer p.close()
time.Sleep(1 * time.Second)
cnt1, err := newContainer("ffmpeg", "source", []string{
"-re",
"-stream_loop", "-1",
"-i", "emptyvideo.ts",
"-c", "copy",
"-f", "rtsp",
"-rtsp_transport", "udp",
"rtsp://" + ownDockerIP + ":8554/test/stream",
})
require.NoError(t, err)
defer cnt1.close()
cnt2, err := newContainer("ffmpeg", "dest", []string{
"-rtsp_transport", "udp",
"-i", "rtsp://testuser:testpass@" + ownDockerIP + ":8554/test/stream",
"-vframes", "1",
"-f", "image2",
"-y", "/dev/null",
})
require.NoError(t, err)
defer cnt2.close()
require.Equal(t, 0, cnt2.wait())
}
func TestAuthIpFail(t *testing.T) {
p, ok := testProgram("paths:\n" +
" all:\n" +

View File

@ -85,15 +85,19 @@ paths:
fallback:
# username required to publish.
# sha256-hashed values can be inserted with the "sha256:" prefix.
publishUser:
# password required to publish.
# sha256-hashed values can be inserted with the "sha256:" prefix.
publishPass:
# ips or networks (x.x.x.x/24) allowed to publish.
publishIps: []
# username required to read.
# sha256-hashed values can be inserted with the "sha256:" prefix.
readUser:
# password required to read.
# sha256-hashed values can be inserted with the "sha256:" prefix.
readPass:
# ips or networks (x.x.x.x/24) allowed to read.
readIps: []