Skip to content

Commit ed906f0

Browse files
committed
support authentication with SHA-256 digest
1 parent c10f7aa commit ed906f0

11 files changed

+218
-118
lines changed

client_play_test.go

+5-7
Original file line numberDiff line numberDiff line change
@@ -1690,17 +1690,15 @@ func TestClientPlayRedirect(t *testing.T) {
16901690
authNonce := "exampleNonce"
16911691
authOpaque := "exampleOpaque"
16921692
authStale := "FALSE"
1693-
authAlg := "MD5"
16941693

16951694
err = conn.WriteResponse(&base.Response{
16961695
Header: base.Header{
16971696
"WWW-Authenticate": headers.Authenticate{
1698-
Method: headers.AuthDigest,
1699-
Realm: authRealm,
1700-
Nonce: authNonce,
1701-
Opaque: &authOpaque,
1702-
Stale: &authStale,
1703-
Algorithm: &authAlg,
1697+
Method: headers.AuthDigestMD5,
1698+
Realm: authRealm,
1699+
Nonce: authNonce,
1700+
Opaque: &authOpaque,
1701+
Stale: &authStale,
17041702
}.Marshal(),
17051703
},
17061704
StatusCode: base.StatusUnauthorized,

pkg/auth/auth_test.go

+7-3
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,15 @@ func TestAuth(t *testing.T) {
2727
[]headers.AuthMethod{headers.AuthBasic},
2828
},
2929
{
30-
"digest",
31-
[]headers.AuthMethod{headers.AuthDigest},
30+
"digest md5",
31+
[]headers.AuthMethod{headers.AuthDigestMD5},
3232
},
3333
{
34-
"both",
34+
"digest sha256",
35+
[]headers.AuthMethod{headers.AuthDigestSHA256},
36+
},
37+
{
38+
"all",
3539
nil,
3640
},
3741
} {

pkg/auth/nonce.go

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package auth
2+
3+
import (
4+
"crypto/rand"
5+
"encoding/hex"
6+
)
7+
8+
// GenerateNonce generates a nonce that can be used in Validate().
9+
func GenerateNonce() (string, error) {
10+
byts := make([]byte, 16)
11+
_, err := rand.Read(byts)
12+
if err != nil {
13+
return "", err
14+
}
15+
16+
return hex.EncodeToString(byts), nil
17+
}

pkg/auth/sender.go

+48-31
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,34 @@ package auth
22

33
import (
44
"fmt"
5-
"strings"
65

76
"github.com/bluenviron/gortsplib/v4/pkg/base"
87
"github.com/bluenviron/gortsplib/v4/pkg/headers"
98
)
109

11-
func findHeader(v base.HeaderValue, prefix string) string {
12-
for _, vi := range v {
13-
if strings.HasPrefix(vi, prefix) {
14-
return vi
10+
func findAuthenticateHeader(auths []headers.Authenticate, method headers.AuthMethod) *headers.Authenticate {
11+
for _, auth := range auths {
12+
if auth.Method == method {
13+
return &auth
1514
}
1615
}
17-
return ""
16+
return nil
17+
}
18+
19+
func pickAuthenticateHeader(auths []headers.Authenticate) (*headers.Authenticate, error) {
20+
if auth := findAuthenticateHeader(auths, headers.AuthDigestSHA256); auth != nil {
21+
return auth, nil
22+
}
23+
24+
if auth := findAuthenticateHeader(auths, headers.AuthDigestMD5); auth != nil {
25+
return auth, nil
26+
}
27+
28+
if auth := findAuthenticateHeader(auths, headers.AuthBasic); auth != nil {
29+
return auth, nil
30+
}
31+
32+
return nil, fmt.Errorf("no authentication methods available")
1833
}
1934

2035
// Sender allows to send credentials.
@@ -27,37 +42,29 @@ type Sender struct {
2742
// NewSender allocates a Sender.
2843
// It requires a WWW-Authenticate header (provided by the server)
2944
// and a set of credentials.
30-
func NewSender(v base.HeaderValue, user string, pass string) (*Sender, error) {
31-
// prefer digest
32-
if v0 := findHeader(v, "Digest"); v0 != "" {
45+
func NewSender(vals base.HeaderValue, user string, pass string) (*Sender, error) {
46+
var auths []headers.Authenticate //nolint:prealloc
47+
48+
for _, v := range vals {
3349
var auth headers.Authenticate
34-
err := auth.Unmarshal(base.HeaderValue{v0})
50+
err := auth.Unmarshal(base.HeaderValue{v})
3551
if err != nil {
36-
return nil, err
52+
continue // ignore unrecognized headers
3753
}
3854

39-
return &Sender{
40-
user: user,
41-
pass: pass,
42-
authenticateHeader: &auth,
43-
}, nil
55+
auths = append(auths, auth)
4456
}
4557

46-
if v0 := findHeader(v, "Basic"); v0 != "" {
47-
var auth headers.Authenticate
48-
err := auth.Unmarshal(base.HeaderValue{v0})
49-
if err != nil {
50-
return nil, err
51-
}
52-
53-
return &Sender{
54-
user: user,
55-
pass: pass,
56-
authenticateHeader: &auth,
57-
}, nil
58+
auth, err := pickAuthenticateHeader(auths)
59+
if err != nil {
60+
return nil, err
5861
}
5962

60-
return nil, fmt.Errorf("no authentication methods available")
63+
return &Sender{
64+
user: user,
65+
pass: pass,
66+
authenticateHeader: auth,
67+
}, nil
6168
}
6269

6370
// AddAuthorization adds the Authorization header to a Request.
@@ -68,16 +75,26 @@ func (se *Sender) AddAuthorization(req *base.Request) {
6875
Method: se.authenticateHeader.Method,
6976
}
7077

71-
if se.authenticateHeader.Method == headers.AuthBasic {
78+
switch se.authenticateHeader.Method {
79+
case headers.AuthBasic:
7280
h.BasicUser = se.user
7381
h.BasicPass = se.pass
74-
} else { // digest
82+
83+
case headers.AuthDigestMD5:
7584
h.Username = se.user
7685
h.Realm = se.authenticateHeader.Realm
7786
h.Nonce = se.authenticateHeader.Nonce
7887
h.URI = urStr
7988
h.Response = md5Hex(md5Hex(se.user+":"+se.authenticateHeader.Realm+":"+se.pass) + ":" +
8089
se.authenticateHeader.Nonce + ":" + md5Hex(string(req.Method)+":"+urStr))
90+
91+
default: // digest SHA-256
92+
h.Username = se.user
93+
h.Realm = se.authenticateHeader.Realm
94+
h.Nonce = se.authenticateHeader.Nonce
95+
h.URI = urStr
96+
h.Response = sha256Hex(sha256Hex(se.user+":"+se.authenticateHeader.Realm+":"+se.pass) + ":" +
97+
se.authenticateHeader.Nonce + ":" + sha256Hex(string(req.Method)+":"+urStr))
8198
}
8299

83100
if req.Header == nil {

pkg/auth/validate.go

+27-48
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ package auth
22

33
import (
44
"crypto/md5"
5-
"crypto/rand"
5+
"crypto/sha256"
66
"encoding/hex"
77
"fmt"
88

@@ -16,41 +16,10 @@ func md5Hex(in string) string {
1616
return hex.EncodeToString(h.Sum(nil))
1717
}
1818

19-
// GenerateNonce generates a nonce that can be used in Validate().
20-
func GenerateNonce() (string, error) {
21-
byts := make([]byte, 16)
22-
_, err := rand.Read(byts)
23-
if err != nil {
24-
return "", err
25-
}
26-
27-
return hex.EncodeToString(byts), nil
28-
}
29-
30-
// GenerateWWWAuthenticate generates a WWW-Authenticate header.
31-
func GenerateWWWAuthenticate(methods []headers.AuthMethod, realm string, nonce string) base.HeaderValue {
32-
if methods == nil {
33-
methods = []headers.AuthMethod{headers.AuthBasic, headers.AuthDigest}
34-
}
35-
36-
var ret base.HeaderValue
37-
for _, m := range methods {
38-
switch m {
39-
case headers.AuthBasic:
40-
ret = append(ret, (&headers.Authenticate{
41-
Method: headers.AuthBasic,
42-
Realm: realm,
43-
}).Marshal()...)
44-
45-
case headers.AuthDigest:
46-
ret = append(ret, headers.Authenticate{
47-
Method: headers.AuthDigest,
48-
Realm: realm,
49-
Nonce: nonce,
50-
}.Marshal()...)
51-
}
52-
}
53-
return ret
19+
func sha256Hex(in string) string {
20+
h := sha256.New()
21+
h.Write([]byte(in))
22+
return hex.EncodeToString(h.Sum(nil))
5423
}
5524

5625
func contains(list []headers.AuthMethod, item headers.AuthMethod) bool {
@@ -73,7 +42,7 @@ func Validate(
7342
nonce string,
7443
) error {
7544
if methods == nil {
76-
methods = []headers.AuthMethod{headers.AuthBasic, headers.AuthDigest}
45+
methods = []headers.AuthMethod{headers.AuthDigestSHA256, headers.AuthDigestMD5, headers.AuthBasic}
7746
}
7847

7948
var auth headers.Authorization
@@ -83,15 +52,8 @@ func Validate(
8352
}
8453

8554
switch {
86-
case auth.Method == headers.AuthBasic && contains(methods, headers.AuthBasic):
87-
if auth.BasicUser != user {
88-
return fmt.Errorf("authentication failed")
89-
}
90-
91-
if auth.BasicPass != pass {
92-
return fmt.Errorf("authentication failed")
93-
}
94-
case auth.Method == headers.AuthDigest && contains(methods, headers.AuthDigest):
55+
case (auth.Method == headers.AuthDigestSHA256 && contains(methods, headers.AuthDigestSHA256)) ||
56+
(auth.Method == headers.AuthDigestMD5 && contains(methods, headers.AuthDigestMD5)):
9557
if auth.Nonce != nonce {
9658
return fmt.Errorf("wrong nonce")
9759
}
@@ -119,12 +81,29 @@ func Validate(
11981
}
12082
}
12183

122-
response := md5Hex(md5Hex(user+":"+realm+":"+pass) +
123-
":" + nonce + ":" + md5Hex(string(req.Method)+":"+ur.String()))
84+
var response string
85+
86+
if auth.Method == headers.AuthDigestSHA256 {
87+
response = sha256Hex(sha256Hex(user+":"+realm+":"+pass) +
88+
":" + nonce + ":" + sha256Hex(string(req.Method)+":"+ur.String()))
89+
} else {
90+
response = md5Hex(md5Hex(user+":"+realm+":"+pass) +
91+
":" + nonce + ":" + md5Hex(string(req.Method)+":"+ur.String()))
92+
}
12493

12594
if auth.Response != response {
12695
return fmt.Errorf("authentication failed")
12796
}
97+
98+
case auth.Method == headers.AuthBasic && contains(methods, headers.AuthBasic):
99+
if auth.BasicUser != user {
100+
return fmt.Errorf("authentication failed")
101+
}
102+
103+
if auth.BasicPass != pass {
104+
return fmt.Errorf("authentication failed")
105+
}
106+
128107
default:
129108
return fmt.Errorf("no supported authentication methods found")
130109
}

pkg/auth/validate_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ func TestValidateAdditionalErrors(t *testing.T) {
4949
"myuser",
5050
"mypass",
5151
nil,
52-
[]headers.AuthMethod{headers.AuthDigest},
52+
[]headers.AuthMethod{headers.AuthDigestMD5},
5353
"IPCAM",
5454
"abcde",
5555
)

pkg/auth/www_authenticate.go

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package auth
2+
3+
import (
4+
"github.com/bluenviron/gortsplib/v4/pkg/base"
5+
"github.com/bluenviron/gortsplib/v4/pkg/headers"
6+
)
7+
8+
// GenerateWWWAuthenticate generates a WWW-Authenticate header.
9+
func GenerateWWWAuthenticate(methods []headers.AuthMethod, realm string, nonce string) base.HeaderValue {
10+
if methods == nil {
11+
methods = []headers.AuthMethod{headers.AuthDigestSHA256, headers.AuthDigestMD5, headers.AuthBasic}
12+
}
13+
14+
var ret base.HeaderValue
15+
for _, m := range methods {
16+
ret = append(ret, headers.Authenticate{
17+
Method: m,
18+
Realm: realm,
19+
Nonce: nonce, // used only by digest
20+
}.Marshal()...)
21+
}
22+
return ret
23+
}

0 commit comments

Comments
 (0)