onvif/digestclient.go
2023-12-18 20:16:21 +01:00

112 lines
2.9 KiB
Go

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
}