package onvif import ( "crypto/md5" "crypto/rand" "encoding/hex" "fmt" "io" "net/http" "strings" ) // DigestClient represents an HTTP client used for making requests authenticated // with http digest authentication. type DigestClient struct { client *http.Client username string password string snonce string realm string qop string nonceCount uint32 } // NewDigestClient returns a DigestClient that wraps a given standard library http Client with the given username and password func NewDigestClient(stdClient *http.Client, username string, password string) *DigestClient { return &DigestClient{ client: stdClient, username: username, password: password, } } func (dc *DigestClient) Do(httpMethod string, endpoint string, soap string) (*http.Response, error) { req, err := createHttpRequest(httpMethod, endpoint, soap) if err != nil { return nil, err } if dc.snonce != "" { req.Header.Set("Authorization", dc.getDigestAuth(req.Method, req.URL.String())) } // Attempt the request using the underlying client resp, err := dc.client.Do(req) if err != nil { return nil, err } if resp.StatusCode != http.StatusUnauthorized { return resp, nil } dc.getDigestParts(resp) // We will need to return the response from another request, so defer a close on this one defer resp.Body.Close() req, err = createHttpRequest(httpMethod, endpoint, soap) if err != nil { return nil, err } req.Header.Set("Authorization", dc.getDigestAuth(req.Method, req.URL.String())) authedResp, err := dc.client.Do(req) if err != nil { return nil, err } return authedResp, nil } func (dc *DigestClient) getDigestParts(resp *http.Response) { result := map[string]string{} authHeader := resp.Header.Get("WWW-Authenticate") if len(authHeader) > 0 { wantedHeaders := []string{"nonce", "realm", "qop"} responseHeaders := strings.Split(authHeader, ",") for _, r := range responseHeaders { for _, w := range wantedHeaders { if strings.Contains(r, w) { result[w] = strings.Split(r, `"`)[1] } } } } dc.snonce = result["nonce"] dc.realm = result["realm"] dc.qop = result["qop"] dc.nonceCount = 0 } func getMD5(text string) string { hasher := md5.New() hasher.Write([]byte(text)) return hex.EncodeToString(hasher.Sum(nil)) } func getCnonce() string { b := make([]byte, 8) io.ReadFull(rand.Reader, b) return fmt.Sprintf("%x", b)[:16] } func (dc *DigestClient) getDigestAuth(method string, uri string) string { ha1 := getMD5(dc.username + ":" + dc.realm + ":" + dc.password) ha2 := getMD5(method + ":" + uri) cnonce := getCnonce() dc.nonceCount++ response := getMD5(fmt.Sprintf("%s:%s:%v:%s:%s:%s", ha1, dc.snonce, dc.nonceCount, cnonce, dc.qop, ha2)) authorization := fmt.Sprintf(`Digest username="%s", realm="%s", nonce="%s", uri="%s", cnonce="%s", nc="%v", qop="%s", response="%s"`, dc.username, dc.realm, dc.snonce, uri, cnonce, dc.nonceCount, dc.qop, response) return authorization }