Skip to content

Commit 3cb3bfe

Browse files
committed
use github.com/icholy/digest to impl digest auth
Signed-off-by: roc <roc@imroc.cc>
1 parent d75da3b commit 3cb3bfe

3 files changed

Lines changed: 19 additions & 208 deletions

File tree

digest.go

Lines changed: 14 additions & 208 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,13 @@ package req
22

33
import (
44
"crypto/md5"
5-
"crypto/rand"
65
"crypto/sha256"
76
"crypto/sha512"
87
"errors"
9-
"fmt"
108
"hash"
11-
"io"
129
"net/http"
13-
"strings"
1410

11+
"github.com/icholy/digest"
1512
"github.com/imroc/req/v3/internal/header"
1613
)
1714

@@ -38,7 +35,7 @@ func handleDigestAuthFunc(username, password string) ResponseMiddleware {
3835
if resp.Err != nil || resp.StatusCode != http.StatusUnauthorized {
3936
return nil
4037
}
41-
auth, err := createDigestAuth(resp.Response, username, password)
38+
auth, err := createDigestAuth(resp.Request.RawRequest, resp.Response, username, password)
4239
if err != nil {
4340
return err
4441
}
@@ -62,217 +59,26 @@ func handleDigestAuthFunc(username, password string) ResponseMiddleware {
6259
req.Header = make(http.Header)
6360
}
6461
req.Header.Set(header.Authorization, auth)
65-
resp.Response, err = client.GetTransport().RoundTrip(&req)
62+
resp.Response, err = client.httpClient.Do(&req)
6663
return err
6764
}
6865
}
6966

70-
func createDigestAuth(resp *http.Response, username, password string) (auth string, err error) {
71-
chal := resp.Header.Get(header.WwwAuthenticate)
72-
if chal == "" {
73-
return "", errDigestBadChallenge
74-
}
75-
76-
c, err := parseChallenge(chal)
67+
func createDigestAuth(req *http.Request, resp *http.Response, username, password string) (auth string, err error) {
68+
chal, err := digest.FindChallenge(resp.Header)
7769
if err != nil {
7870
return "", err
7971
}
80-
81-
// Form credentials based on the challenge
82-
cr := newCredentials(resp.Request.URL.RequestURI(), resp.Request.Method, username, password, c)
83-
auth, err = cr.authorize()
84-
return
85-
}
86-
87-
func newCredentials(digestURI, method, username, password string, c *challenge) *credentials {
88-
return &credentials{
89-
username: username,
90-
userhash: c.userhash,
91-
realm: c.realm,
92-
nonce: c.nonce,
93-
digestURI: digestURI,
94-
algorithm: c.algorithm,
95-
sessionAlg: strings.HasSuffix(c.algorithm, "-sess"),
96-
opaque: c.opaque,
97-
messageQop: c.qop,
98-
nc: 0,
99-
method: method,
100-
password: password,
101-
}
102-
}
103-
104-
type challenge struct {
105-
realm string
106-
domain string
107-
nonce string
108-
opaque string
109-
stale string
110-
algorithm string
111-
qop string
112-
userhash string
113-
}
114-
115-
func parseChallenge(input string) (*challenge, error) {
116-
const ws = " \n\r\t"
117-
const qs = `"`
118-
s := strings.Trim(input, ws)
119-
if !strings.HasPrefix(s, "Digest ") {
120-
return nil, errDigestBadChallenge
121-
}
122-
s = strings.Trim(s[7:], ws)
123-
sl := strings.Split(s, ",")
124-
c := &challenge{}
125-
var r []string
126-
for i := range sl {
127-
r = strings.SplitN(strings.TrimSpace(sl[i]), "=", 2)
128-
if len(r) != 2 {
129-
return nil, errDigestBadChallenge
130-
}
131-
switch r[0] {
132-
case "realm":
133-
c.realm = strings.Trim(r[1], qs)
134-
case "domain":
135-
c.domain = strings.Trim(r[1], qs)
136-
case "nonce":
137-
c.nonce = strings.Trim(r[1], qs)
138-
case "opaque":
139-
c.opaque = strings.Trim(r[1], qs)
140-
case "stale":
141-
c.stale = strings.Trim(r[1], qs)
142-
case "algorithm":
143-
c.algorithm = strings.Trim(r[1], qs)
144-
case "qop":
145-
c.qop = strings.Trim(r[1], qs)
146-
case "charset":
147-
if strings.ToUpper(strings.Trim(r[1], qs)) != "UTF-8" {
148-
return nil, errDigestCharset
149-
}
150-
case "userhash":
151-
c.userhash = strings.Trim(r[1], qs)
152-
default:
153-
return nil, errDigestBadChallenge
154-
}
155-
}
156-
return c, nil
157-
}
158-
159-
type credentials struct {
160-
username string
161-
userhash string
162-
realm string
163-
nonce string
164-
digestURI string
165-
algorithm string
166-
sessionAlg bool
167-
cNonce string
168-
opaque string
169-
messageQop string
170-
nc int
171-
method string
172-
password string
173-
}
174-
175-
func (c *credentials) authorize() (string, error) {
176-
if _, ok := hashFuncs[c.algorithm]; !ok {
177-
return "", errDigestAlgNotSupported
178-
}
179-
180-
if err := c.validateQop(); err != nil {
181-
return "", err
182-
}
183-
184-
resp, err := c.resp()
72+
cred, err := digest.Digest(chal, digest.Options{
73+
Username: username,
74+
Password: password,
75+
Method: req.Method,
76+
URI: req.URL.RequestURI(),
77+
GetBody: req.GetBody,
78+
Count: 1,
79+
})
18580
if err != nil {
18681
return "", err
18782
}
188-
189-
sl := make([]string, 0, 10)
190-
if c.userhash == "true" {
191-
// RFC 7616 3.4.4
192-
c.username = c.h(fmt.Sprintf("%s:%s", c.username, c.realm))
193-
sl = append(sl, fmt.Sprintf(`userhash=%s`, c.userhash))
194-
}
195-
sl = append(sl, fmt.Sprintf(`username="%s"`, c.username))
196-
sl = append(sl, fmt.Sprintf(`realm="%s"`, c.realm))
197-
sl = append(sl, fmt.Sprintf(`nonce="%s"`, c.nonce))
198-
sl = append(sl, fmt.Sprintf(`uri="%s"`, c.digestURI))
199-
sl = append(sl, fmt.Sprintf(`response="%s"`, resp))
200-
sl = append(sl, fmt.Sprintf(`algorithm=%s`, c.algorithm))
201-
if c.opaque != "" {
202-
sl = append(sl, fmt.Sprintf(`opaque="%s"`, c.opaque))
203-
}
204-
if c.messageQop != "" {
205-
sl = append(sl, fmt.Sprintf("qop=%s", c.messageQop))
206-
sl = append(sl, fmt.Sprintf("nc=%08x", c.nc))
207-
sl = append(sl, fmt.Sprintf(`cnonce="%s"`, c.cNonce))
208-
}
209-
210-
return fmt.Sprintf("Digest %s", strings.Join(sl, ", ")), nil
211-
}
212-
213-
func (c *credentials) validateQop() error {
214-
// Currently only supporting auth quality of protection. TODO: add auth-int support
215-
if c.messageQop == "" {
216-
return nil
217-
}
218-
possibleQops := strings.Split(c.messageQop, ", ")
219-
var authSupport bool
220-
for _, qop := range possibleQops {
221-
if qop == "auth" {
222-
authSupport = true
223-
break
224-
}
225-
}
226-
if !authSupport {
227-
return errDigestQopNotSupported
228-
}
229-
230-
return nil
231-
}
232-
233-
func (c *credentials) h(data string) string {
234-
hfCtor := hashFuncs[c.algorithm]
235-
hf := hfCtor()
236-
_, _ = hf.Write([]byte(data)) // Hash.Write never returns an error
237-
return fmt.Sprintf("%x", hf.Sum(nil))
238-
}
239-
240-
func (c *credentials) resp() (string, error) {
241-
c.nc++
242-
243-
b := make([]byte, 16)
244-
_, err := io.ReadFull(rand.Reader, b)
245-
if err != nil {
246-
return "", err
247-
}
248-
c.cNonce = fmt.Sprintf("%x", b)[:32]
249-
250-
ha1 := c.ha1()
251-
ha2 := c.ha2()
252-
253-
if len(c.messageQop) == 0 {
254-
return c.h(fmt.Sprintf("%s:%s:%s", ha1, c.nonce, ha2)), nil
255-
}
256-
return c.kd(ha1, fmt.Sprintf("%s:%08x:%s:%s:%s",
257-
c.nonce, c.nc, c.cNonce, c.messageQop, ha2)), nil
258-
}
259-
260-
func (c *credentials) kd(secret, data string) string {
261-
return c.h(fmt.Sprintf("%s:%s", secret, data))
262-
}
263-
264-
// RFC 7616 3.4.2
265-
func (c *credentials) ha1() string {
266-
ret := c.h(fmt.Sprintf("%s:%s:%s", c.username, c.realm, c.password))
267-
if c.sessionAlg {
268-
return c.h(fmt.Sprintf("%s:%s:%s", ret, c.nonce, c.cNonce))
269-
}
270-
271-
return ret
272-
}
273-
274-
// RFC 7616 3.4.3
275-
func (c *credentials) ha2() string {
276-
// currently no auth-int support
277-
return c.h(fmt.Sprintf("%s:%s", c.method, c.digestURI))
83+
return cred.String(), nil
27884
}

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ go 1.23.0
55
require (
66
github.com/andybalholm/brotli v1.1.1
77
github.com/hashicorp/go-multierror v1.1.1
8+
github.com/icholy/digest v1.1.0
89
github.com/klauspost/compress v1.18.0
910
github.com/quic-go/qpack v0.5.1
1011
github.com/quic-go/quic-go v0.51.0

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY
1717
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
1818
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
1919
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
20+
github.com/icholy/digest v1.1.0 h1:HfGg9Irj7i+IX1o1QAmPfIBNu/Q5A5Tu3n/MED9k9H4=
21+
github.com/icholy/digest v1.1.0/go.mod h1:QNrsSGQ5v7v9cReDI0+eyjsXGUoRSUZQHeQ5C4XLa0Y=
2022
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
2123
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
2224
github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus=
@@ -59,3 +61,5 @@ google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwl
5961
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
6062
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
6163
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
64+
gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
65+
gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=

0 commit comments

Comments
 (0)