-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathdigest.go
283 lines (243 loc) · 8.15 KB
/
digest.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
// Package digest provides a drop-in replacement for http.Client that supports HTTP Digest
// auth for GET and POST (and other) HTTP methods
package digest
import (
"bytes"
"crypto/md5"
"crypto/rand"
"encoding/base64"
"encoding/hex"
"fmt"
"io"
"net/http"
"net/url"
"strings"
)
// Type Client is a wrapper around http.Client
type Client struct {
*http.Client
User string
Password string
}
// NewClient returns a new digest Client instance. If c is nil, a new default
// client is created. Otherwise, it wraps the given one
func NewClient(c *http.Client, Username string, Password string) *Client {
if c == nil {
c = &http.Client{}
}
return &Client{Client: c, User: Username, Password: Password}
}
// Get issues a GET to the specified URL. If the response is one of the
// following redirect codes, Get follows the redirect after calling the
// Client's CheckRedirect function:
//
// 301 (Moved Permanently)
// 302 (Found)
// 303 (See Other)
// 307 (Temporary Redirect)
// 308 (Permanent Redirect)
//
// An error is returned if the Client's CheckRedirect function fails
// or if there was an HTTP protocol error. A non-2xx response doesn't
// cause an error.
//
// When err is nil, resp always contains a non-nil resp.Body.
// Caller should close resp.Body when done reading from it.
//
// To make a request with custom headers, use http.NewRequest and Client.Do.
func (c *Client) Get(url string) (resp *http.Response, err error) {
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
return c.Do(req)
}
// Head issues a HEAD to the specified URL. If the response is one of the
// following redirect codes, Head follows the redirect after calling the
// Client's CheckRedirect function:
//
// 301 (Moved Permanently)
// 302 (Found)
// 303 (See Other)
// 307 (Temporary Redirect)
// 308 (Permanent Redirect)
func (c *Client) Head(url string) (resp *http.Response, err error) {
req, err := http.NewRequest("HEAD", url, nil)
if err != nil {
return nil, err
}
return c.Do(req)
}
// Post issues a POST to the specified URL.
//
// Caller should close resp.Body when done reading from it.
//
// If the provided body is an io.Closer, it is closed after the
// request.
//
// To set custom headers, use http.NewRequest and Client.Do.
//
// See the Client.Do method documentation for details on how redirects
// are handled.
func (c *Client) Post(url, contentType string, body io.Reader) (resp *http.Response, err error) {
req, err := http.NewRequest("POST", url, body)
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", contentType)
return c.Do(req)
}
// PostForm issues a POST to the specified URL,
// with data's keys and values URL-encoded as the request body.
//
// The Content-Type header is set to application/x-www-form-urlencoded.
// To set other headers, use http.NewRequest and Client.Do.
//
// When err is nil, resp always contains a non-nil resp.Body.
// Caller should close resp.Body when done reading from it.
//
// See the Client.Do method documentation for details on how redirects
// are handled.
func (c *Client) PostForm(url string, data url.Values) (resp *http.Response, err error) {
return c.Post(url, "application/x-www-form-urlencoded", strings.NewReader(data.Encode()))
}
// Do sends an HTTP request and returns an HTTP response, following
// policy (such as redirects, cookies, auth) as configured on the
// client.
//
// If Username and Password are specified in the client, Do performs HTTP
// Digest authentication against the web server
//
// An error is returned if caused by client policy (such as CheckRedirect), or
// failure to speak HTTP (such as a network connectivity problem). A non-2xx
// status code doesn't cause an error.
//
// If the returned error is nil, the Response will contain a non-nil Body which
// the user is expected to close. If the Body is not closed, the Client's
// underlying RoundTripper (typically Transport) may not be able to re-use a
// persistent TCP connection to the server for a subsequent "keep-alive"
// request.
//
// The request Body, if non-nil, will be closed by the underlying Transport,
// even on errors.
//
// On error, any Response can be ignored. A non-nil Response with a non-nil
// error only occurs when CheckRedirect fails, and even then the returned
// Response.Body is already closed.
//
// Generally Get, Post, or PostForm will be used instead of Do.
//
// If the server replies with a redirect, the Client first uses the
// CheckRedirect function to determine whether the redirect should be followed.
// If permitted, a 301, 302, or 303 redirect causes subsequent requests to use
// HTTP method GET (or HEAD if the original request was HEAD), with no body. A
// 307 or 308 redirect preserves the original HTTP method and body, provided
// that the Request.GetBody function is defined. The http.NewRequest function
// automatically sets GetBody for common standard library body types.
func (c *Client) Do(r *http.Request) (resp *http.Response, err error) {
// If no user/pass is set, just wrap *http.Client
if c.User == "" && c.Password == "" {
return c.Client.Do(r)
}
// Copy the original request, except the body
initreq, err := http.NewRequest(r.Method, r.URL.String(), nil)
if err != nil {
return nil, err
}
// Copy headers
(*initreq).Header = (*r).Header
resp, err = c.Client.Do(initreq)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusUnauthorized {
if r.Body != nil {
resp, err = c.Client.Do(r)
}
} else {
digestParts := digestParts(r, resp)
digestParts["uri"] = r.URL.Path
digestParts["method"] = r.Method
digestParts["username"] = c.User
digestParts["password"] = c.Password
r.Header.Set("Authorization", getDigestAuth(digestParts, r))
resp, err = c.Client.Do(r)
}
return resp, err
}
func digestParts(req *http.Request, resp *http.Response) (res map[string]string) {
res = make(map[string]string)
// 'qop' header can be 'auth, auth-int', so need to handle that. Only use auth-int if body is not null
if len(resp.Header["Www-Authenticate"]) > 0 {
wantedHeaders := []string{"nonce", "realm", "qop", "algorithm", "opaque"}
responseHeaders := strings.Split(resp.Header["Www-Authenticate"][0], ",")
for _, r := range responseHeaders {
r = strings.TrimSpace(r)
for _, w := range wantedHeaders {
if strings.Contains(r, w) {
res[w] = strings.Split(r, `"`)[1]
if w == "qop" {
if strings.Contains(res[w], "auth-int") && req.Body != nil {
res[w] = "auth-int"
} else if strings.Contains(res[w], "auth") {
res[w] = "auth"
}
}
}
}
}
}
return res
}
func getMD5(text string) string {
hasher := md5.New()
hasher.Write([]byte(text))
return hex.EncodeToString(hasher.Sum(nil))
}
func randomKey() string {
k := make([]byte, 12)
for bytes := 0; bytes < len(k); {
n, err := rand.Read(k[bytes:])
if err != nil {
panic("rand.Read() failed")
}
bytes += n
}
return base64.StdEncoding.EncodeToString(k)
}
func getDigestAuth(d map[string]string, r *http.Request) (auth string) {
var ha1 string
var ha2 string
cnonce := randomKey()
//ha1
switch d["algorithm"] {
case "MD5":
ha1 = getMD5(fmt.Sprintf("%s:%s:%s", d["username"], d["realm"], d["password"]))
case "MD5-sess":
ha1 = getMD5(fmt.Sprintf("%s:%s:%s",
getMD5(fmt.Sprintf("%s:%s:%s", d["username"], d["realm"], d["password"])),
d["nonce"], cnonce))
}
// ha2
switch d["qop"] {
case "auth-int":
buf := new(bytes.Buffer)
buf.ReadFrom(r.Body)
s := buf.String()
ha2 = getMD5(fmt.Sprintf("%s:%s:%s", d["method"], d["uri"], getMD5(s)))
case "auth", "":
ha2 = getMD5(fmt.Sprintf("%s:%s", d["method"], d["uri"]))
}
var response string
nonceCount := 1
// determine response
switch d["qop"] {
case "auth", "auth-int":
response = getMD5(fmt.Sprintf("%s:%s:%08d:%s:%s:%s", ha1, d["nonce"], nonceCount, cnonce, d["qop"], ha2))
case "":
response = getMD5(fmt.Sprintf("%s:%s:%s", ha1, d["nonce"], ha2))
}
auth = fmt.Sprintf(`Digest username="%s", realm="%s", nonce="%s", uri="%s", cnonce="%s", nc=%08d, qop="%s", response="%s", opaque="%s", algorithm="%s"`,
d["username"], d["realm"], d["nonce"], d["uri"], cnonce, nonceCount, d["qop"], response, d["opaque"], d["algorithm"])
return auth
}