Skip to content

Commit a7e794f

Browse files
committed
Add digest auth option to downloader integration
1 parent 4aff032 commit a7e794f

File tree

5 files changed

+128
-1
lines changed

5 files changed

+128
-1
lines changed

homeassistant/components/downloader/const.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,12 @@
77
DOMAIN = "downloader"
88
DEFAULT_NAME = "Downloader"
99
CONF_DOWNLOAD_DIR = "download_dir"
10+
ATTR_DIGEST_AUTH = "digest_auth"
1011
ATTR_FILENAME = "filename"
12+
ATTR_DIGEST_PASSWORD = "digest_password"
1113
ATTR_SUBDIR = "subdir"
1214
ATTR_URL = "url"
15+
ATTR_DIGEST_USERNAME = "digest_username"
1316
ATTR_OVERWRITE = "overwrite"
1417

1518
CONF_DOWNLOAD_DIR = "download_dir"

homeassistant/components/downloader/services.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import threading
99

1010
import requests
11+
from requests.auth import HTTPDigestAuth
1112
import voluptuous as vol
1213

1314
from homeassistant.core import HomeAssistant, ServiceCall, callback
@@ -17,6 +18,9 @@
1718

1819
from .const import (
1920
_LOGGER,
21+
ATTR_DIGEST_AUTH,
22+
ATTR_DIGEST_PASSWORD,
23+
ATTR_DIGEST_USERNAME,
2024
ATTR_FILENAME,
2125
ATTR_OVERWRITE,
2226
ATTR_SUBDIR,
@@ -40,6 +44,12 @@ def do_download() -> None:
4044
try:
4145
url = service.data[ATTR_URL]
4246

47+
digest_auth = service.data.get(ATTR_DIGEST_AUTH)
48+
49+
digest_username = service.data.get(ATTR_DIGEST_USERNAME)
50+
51+
digest_password = service.data.get(ATTR_DIGEST_PASSWORD)
52+
4353
subdir = service.data.get(ATTR_SUBDIR)
4454

4555
filename = service.data.get(ATTR_FILENAME)
@@ -52,7 +62,18 @@ def do_download() -> None:
5262

5363
final_path = None
5464

55-
req = requests.get(url, stream=True, timeout=10)
65+
auth = (
66+
HTTPDigestAuth(digest_username, digest_password)
67+
if digest_auth and digest_username and digest_password
68+
else None
69+
)
70+
71+
req = requests.get(
72+
url=url,
73+
stream=True,
74+
timeout=10,
75+
auth=auth,
76+
)
5677

5778
if req.status_code != HTTPStatus.OK:
5879
_LOGGER.warning(
@@ -151,9 +172,12 @@ def async_setup_services(hass: HomeAssistant) -> None:
151172
download_file,
152173
schema=vol.Schema(
153174
{
175+
vol.Optional(ATTR_DIGEST_AUTH, default=False): cv.boolean,
154176
vol.Optional(ATTR_FILENAME): cv.string,
177+
vol.Optional(ATTR_DIGEST_PASSWORD): cv.string,
155178
vol.Optional(ATTR_SUBDIR): cv.string,
156179
vol.Required(ATTR_URL): cv.url,
180+
vol.Optional(ATTR_DIGEST_USERNAME): cv.string,
157181
vol.Optional(ATTR_OVERWRITE, default=False): cv.boolean,
158182
}
159183
),

homeassistant/components/downloader/services.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,15 @@ download_file:
55
example: "http://example.org/myfile"
66
selector:
77
text:
8+
digest_auth:
9+
selector:
10+
boolean:
11+
digest_username:
12+
selector:
13+
text:
14+
digest_password:
15+
selector:
16+
text:
817
subdir:
918
example: "download_dir"
1019
selector:

homeassistant/components/downloader/strings.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,18 @@
2121
"name": "[%key:common::config_flow::data::url%]",
2222
"description": "The URL of the file to download."
2323
},
24+
"digest_auth": {
25+
"name": "Use Digest Authentication",
26+
"description": "Whether to use digest authentication."
27+
},
28+
"digest_username": {
29+
"name": "Digest Authentication Username",
30+
"description": "Username to use with digest authentication."
31+
},
32+
"digest_password": {
33+
"name": "Digest Authentication Password",
34+
"description": "Password to use with digest authentication."
35+
},
2436
"subdir": {
2537
"name": "Subdirectory",
2638
"description": "Relative download path."
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
"""Test downloader action."""
2+
3+
from http import HTTPStatus
4+
from unittest.mock import mock_open, patch
5+
6+
import requests_mock
7+
8+
from homeassistant.components.downloader.const import (
9+
ATTR_DIGEST_AUTH,
10+
ATTR_DIGEST_PASSWORD,
11+
ATTR_DIGEST_USERNAME,
12+
ATTR_URL,
13+
CONF_DOWNLOAD_DIR,
14+
DOMAIN,
15+
SERVICE_DOWNLOAD_FILE,
16+
)
17+
from homeassistant.core import HomeAssistant
18+
19+
from tests.common import MockConfigEntry
20+
21+
22+
def digest_challenge_matcher(request):
23+
"""Match requests with no auth header."""
24+
return "Authorization" not in request.headers
25+
26+
27+
def digest_response_matcher(request):
28+
"""Match requests with an auth header."""
29+
return "Authorization" in request.headers
30+
31+
32+
async def test_digest_auth(hass: HomeAssistant) -> None:
33+
"""Test digest authentication in downloader action."""
34+
35+
TEST_URL = "http://example.com/protected-resource"
36+
37+
config_entry = MockConfigEntry(
38+
domain=DOMAIN,
39+
data={
40+
CONF_DOWNLOAD_DIR: "/test_dir",
41+
},
42+
)
43+
config_entry.add_to_hass(hass)
44+
with patch("os.path.isdir", return_value=True):
45+
await hass.config_entries.async_setup(config_entry.entry_id)
46+
47+
with requests_mock.Mocker() as mock:
48+
mock.get(
49+
TEST_URL,
50+
additional_matcher=digest_challenge_matcher,
51+
headers={"WWW-Authenticate": 'Digest realm="example.com", nonce="test"'},
52+
status_code=HTTPStatus.UNAUTHORIZED,
53+
)
54+
55+
mock.get(
56+
TEST_URL,
57+
additional_matcher=digest_response_matcher,
58+
status_code=HTTPStatus.OK,
59+
)
60+
61+
with patch("builtins.open", new=mock_open(), create=True):
62+
await hass.services.async_call(
63+
DOMAIN,
64+
SERVICE_DOWNLOAD_FILE,
65+
{
66+
ATTR_URL: TEST_URL,
67+
ATTR_DIGEST_AUTH: True,
68+
ATTR_DIGEST_USERNAME: "test",
69+
ATTR_DIGEST_PASSWORD: "test",
70+
},
71+
blocking=True,
72+
)
73+
74+
assert mock.called
75+
76+
assert (
77+
"Authorization" in mock.last_request.headers
78+
and mock.last_request.headers["Authorization"].startswith("Digest")
79+
)

0 commit comments

Comments
 (0)