Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add support for lowercase digest algorithms #14

Merged
merged 1 commit into from
Nov 23, 2024

Conversation

modiius
Copy link
Contributor

@modiius modiius commented Nov 22, 2024

Thank you for the library! I've been using it to handle authentication for IP cameras, but encountered an issue while trying to authenticate with an Acti IP camera. Here is the code:

import (
	"bytes"
	"fmt"
	"io"
	"net/http"
	"net/url"

	"github.com/icholy/digest"
)

type Client struct {
	baseURL  *url.URL
	username string
	password string
}

func NewClient(host, username, password string) (*Client, error) {
	u, err := url.Parse("http://" + host)
	if err != nil {
		return nil, fmt.Errorf("error parsing URL: %v", err)
	}
	return &Client{
		baseURL:  u,
		username: username,
		password: password,
	}, nil
}

func (c Client) Send(request []byte, uriPath string) ([]byte, error) {
	u, err := url.Parse(c.baseURL.String() + "/" + uriPath)
	if err != nil {
		return nil, fmt.Errorf("error parsing URL: %v", err)
	}

	req, err := http.NewRequest("POST", u.String(), bytes.NewBuffer(request))
	if err != nil {
		return nil, fmt.Errorf("error creating HTTP request: %v", err)
	}

	req.Header.Set("Content-Type", "application/soap+xml; charset=utf-8")
	req.Header.Set("User-Agent", "agent/1.0")

	client := &http.Client{
		Transport: &digest.Transport{
			Username: c.username,
			Password: c.password,
		},
	}
	resp, err := client.Do(req)
	if err != nil {
		return nil, fmt.Errorf("error sending HTTP request: %v", err)
	}
	defer resp.Body.Close()

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("error reading response body: %v", err)
	}

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("bad response status: %v, body: %s", resp.Status, string(body))
	}

	return body, nil
}

The returned error wasn't too helpful:

error reading response body: http: read on closed response body

After some investigation, it turned out that the initial digest 401 response from the camera had a lowercase md5 and the code only checks against an uppercase MD5

curl ... -v
< HTTP/1.1 401 Unauthorized
< Server: Httpd v1.0 05may2008
< Content-Type: application/soap+xml; charset=utf-8
< Date: Fri, 22 Nov 2024 13:59:20 GMT
< Last-Modified: Fri, 22 Nov 2024 13:59:20 GMT
< Accept-Ranges: bytes
< Connection: Close
< Cache-Control: no-cache,no-store
< WWW-Authenticate: Basic realm="Http Server"
< WWW-Authenticate: Digest realm="Http Server", nonce="f0ef6bfecc196903b2befc6a66b59b8a", algorithm=md5, qop="auth"

@icholy
Copy link
Owner

icholy commented Nov 22, 2024

I previously rejected a similar PR here: #11

But since this is coming up again, maybe it's worth reconsidering. Can you test with curl and see if it accepts the lower case md5 algorithm value?

@modiius
Copy link
Contributor Author

modiius commented Nov 22, 2024

The popular requests python library converts the algorithm to uppercase
https://github.com/psf/requests/blob/main/src/requests/auth.py#L141

_algorithm = algorithm.upper()

On your rejected PR #11 - I can't seem to find where in the spec they stipulate uppercase only or that 'md5 is not valid'. The ambiguity in the spec is likely where these small differences creep in.

The curl command seems to pass the auth part and fail at bad request (which seems like some server-side issue). Postman works and this python script works as well:

import requests
from requests.auth import HTTPDigestAuth

url = 'http://10.50.8.10/onvif/device_service'
username = 'username'
password = 'password'

payload = '''<?xml version="1.0" encoding="UTF-8"?>
<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope" xmlns:wsdl="http://www.onvif.org/ver10/media/wsdl">
  <soap:Body>
    <wsdl:GetProfiles></wsdl:GetProfiles>
  </soap:Body>
</soap:Envelope>'''

headers = {
    'Content-Type': 'application/xml'
}

response = requests.post(url, data=payload, headers=headers, auth=HTTPDigestAuth(username, password))

if response.status_code == 200:
    print("Request successful:")
    print(response.text)
else:
    print(f"Request failed with status code {response.status_code}:")
    print(response.text)

Hope that helps :)

@icholy
Copy link
Owner

icholy commented Nov 22, 2024

On your rejected PR #11 - I can't seem to find where in the spec they stipulate uppercase only or that 'md5 is not valid'.

Specifications define what is allowed, not what isn't allowed. If the spec shows "MD5", then "MD5" is what's valid. It won't list "md5", "Md5", or other variations as invalid because that's not how specs work - they only define the valid patterns.

@icholy
Copy link
Owner

icholy commented Nov 22, 2024

Situations like these are the unfortunate outcome of following Postel's Law. But it seems like we should probably follow suit and uppercase if that's what everyone else is doing.

digest.go Show resolved Hide resolved
@icholy icholy merged commit 8f62119 into icholy:master Nov 23, 2024
1 check passed
@icholy
Copy link
Owner

icholy commented Nov 23, 2024

Tagged in v1.0.1

@modiius modiius deleted the support-lowercase-digest-algorithms branch November 23, 2024 04:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants