From a87fedc0af3e2a9bcec745f924fb4534c3b57cea Mon Sep 17 00:00:00 2001 From: On Freund Date: Thu, 3 Sep 2020 23:41:24 +0300 Subject: [PATCH] Automatically configure HTTP auth type in ONVIF snapshots (#38729) * Allow selection of HTTP auth type in ONVIF snapshots * Auto populate snapshot auth * Fix no auth case * Add missing return --- homeassistant/components/onvif/__init__.py | 42 +++++++++++++++++++ homeassistant/components/onvif/camera.py | 13 +++++- homeassistant/components/onvif/const.py | 1 + .../components/onvif/translations/en.json | 8 ++-- 4 files changed, 58 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/onvif/__init__.py b/homeassistant/components/onvif/__init__.py index bb8008e1fef..93e948b770b 100644 --- a/homeassistant/components/onvif/__init__.py +++ b/homeassistant/components/onvif/__init__.py @@ -1,6 +1,9 @@ """The ONVIF integration.""" import asyncio +import requests +from requests.auth import HTTPDigestAuth +from urllib3.exceptions import ReadTimeoutError import voluptuous as vol from homeassistant.components.ffmpeg import CONF_EXTRA_ARGUMENTS @@ -12,6 +15,8 @@ from homeassistant.const import ( CONF_PORT, CONF_USERNAME, EVENT_HOMEASSISTANT_STOP, + HTTP_BASIC_AUTHENTICATION, + HTTP_DIGEST_AUTHENTICATION, ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady @@ -19,6 +24,7 @@ from homeassistant.helpers import config_per_platform from .const import ( CONF_RTSP_TRANSPORT, + CONF_SNAPSHOT_AUTH, DEFAULT_ARGUMENTS, DEFAULT_NAME, DEFAULT_PASSWORD, @@ -76,6 +82,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): if not device.available: raise ConfigEntryNotReady() + if not entry.data.get(CONF_SNAPSHOT_AUTH): + await async_populate_snapshot_auth(hass, device, entry) + hass.data[DOMAIN][entry.unique_id] = device platforms = ["camera"] @@ -113,6 +122,39 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): ) +async def _get_snapshot_auth(hass, device, entry): + if not (device.username and device.password): + return HTTP_DIGEST_AUTHENTICATION + + snapshot_uri = await device.async_get_snapshot_uri(device.profiles[0]) + auth = HTTPDigestAuth(device.username, device.password) + + def _get(): + # so we can handle keyword arguments + return requests.get(snapshot_uri, timeout=1, auth=auth) + + try: + response = await hass.async_add_executor_job(_get) + + if response.status_code == 401: + return HTTP_BASIC_AUTHENTICATION + + return HTTP_DIGEST_AUTHENTICATION + except requests.exceptions.Timeout: + return HTTP_BASIC_AUTHENTICATION + except requests.exceptions.ConnectionError as error: + if isinstance(error.args[0], ReadTimeoutError): + return HTTP_BASIC_AUTHENTICATION + return HTTP_DIGEST_AUTHENTICATION + + +async def async_populate_snapshot_auth(hass, device, entry): + """Check if digest auth for snapshots is possible.""" + auth = await _get_snapshot_auth(hass, device, entry) + new_data = {**entry.data, CONF_SNAPSHOT_AUTH: auth} + hass.config_entries.async_update_entry(entry, data=new_data) + + async def async_populate_options(hass, entry): """Populate default options for device.""" options = { diff --git a/homeassistant/components/onvif/camera.py b/homeassistant/components/onvif/camera.py index 7f97e0fbea4..5c2f89adf32 100644 --- a/homeassistant/components/onvif/camera.py +++ b/homeassistant/components/onvif/camera.py @@ -4,11 +4,12 @@ import asyncio from haffmpeg.camera import CameraMjpeg from haffmpeg.tools import IMAGE_JPEG, ImageFrame import requests -from requests.auth import HTTPDigestAuth +from requests.auth import HTTPBasicAuth, HTTPDigestAuth import voluptuous as vol from homeassistant.components.camera import SUPPORT_STREAM, Camera from homeassistant.components.ffmpeg import CONF_EXTRA_ARGUMENTS, DATA_FFMPEG +from homeassistant.const import HTTP_BASIC_AUTHENTICATION from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream @@ -24,6 +25,7 @@ from .const import ( ATTR_TILT, ATTR_ZOOM, CONF_RTSP_TRANSPORT, + CONF_SNAPSHOT_AUTH, CONTINUOUS_MOVE, DIR_DOWN, DIR_LEFT, @@ -79,6 +81,10 @@ class ONVIFCameraEntity(ONVIFBaseEntity, Camera): self.stream_options[CONF_RTSP_TRANSPORT] = device.config_entry.options.get( CONF_RTSP_TRANSPORT ) + self._basic_auth = ( + device.config_entry.data.get(CONF_SNAPSHOT_AUTH) + == HTTP_BASIC_AUTHENTICATION + ) self._stream_uri = None self._snapshot_uri = None @@ -115,7 +121,10 @@ class ONVIFCameraEntity(ONVIFBaseEntity, Camera): if self.device.capabilities.snapshot: auth = None if self.device.username and self.device.password: - auth = HTTPDigestAuth(self.device.username, self.device.password) + if self._basic_auth: + auth = HTTPBasicAuth(self.device.username, self.device.password) + else: + auth = HTTPDigestAuth(self.device.username, self.device.password) def fetch(): """Read image from a URL.""" diff --git a/homeassistant/components/onvif/const.py b/homeassistant/components/onvif/const.py index ddc1cc22801..2ac78622f05 100644 --- a/homeassistant/components/onvif/const.py +++ b/homeassistant/components/onvif/const.py @@ -13,6 +13,7 @@ DEFAULT_ARGUMENTS = "-pred 1" CONF_DEVICE_ID = "deviceid" CONF_RTSP_TRANSPORT = "rtsp_transport" +CONF_SNAPSHOT_AUTH = "snapshot_auth" RTSP_TRANS_PROTOCOLS = ["tcp", "udp", "udp_multicast", "http"] diff --git a/homeassistant/components/onvif/translations/en.json b/homeassistant/components/onvif/translations/en.json index a20b1fcb7e4..df47900fbc5 100644 --- a/homeassistant/components/onvif/translations/en.json +++ b/homeassistant/components/onvif/translations/en.json @@ -13,8 +13,8 @@ "step": { "auth": { "data": { - "password": "Password", - "username": "Username" + "password": "[%key:common::config_flow::data::password%]", + "username": "[%key:common::config_flow::data::username%]" }, "title": "Configure authentication" }, @@ -33,9 +33,9 @@ }, "manual_input": { "data": { - "host": "Host", + "host": "[%key:common::config_flow::data::host%]", "name": "Name", - "port": "Port" + "port": "[%key:common::config_flow::data::port%]" }, "title": "Configure ONVIF device" },