Dont substitute user/pass for relative stream urls on generic camera (#74201)

Co-authored-by: Dave T <davet2001@users.noreply.github.com>
This commit is contained in:
Dave T 2022-07-03 21:26:00 +01:00 committed by GitHub
parent 57114c1a55
commit 737a1fd9fa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 68 additions and 14 deletions

View File

@ -228,7 +228,13 @@ class GenericCamera(Camera):
try:
stream_url = self._stream_source.async_render(parse_result=False)
url = yarl.URL(stream_url)
if not url.user and not url.password and self._username and self._password:
if (
not url.user
and not url.password
and self._username
and self._password
and url.is_absolute()
):
url = url.with_user(self._username).with_password(self._password)
return str(url)
except TemplateError as err:

View File

@ -150,6 +150,12 @@ async def async_test_still(
except TemplateError as err:
_LOGGER.warning("Problem rendering template %s: %s", url, err)
return {CONF_STILL_IMAGE_URL: "template_error"}, None
try:
yarl_url = yarl.URL(url)
except ValueError:
return {CONF_STILL_IMAGE_URL: "malformed_url"}, None
if not yarl_url.is_absolute():
return {CONF_STILL_IMAGE_URL: "relative_url"}, None
verify_ssl = info[CONF_VERIFY_SSL]
auth = generate_auth(info)
try:
@ -222,7 +228,12 @@ async def async_test_stream(
if info.get(CONF_USE_WALLCLOCK_AS_TIMESTAMPS):
stream_options[CONF_USE_WALLCLOCK_AS_TIMESTAMPS] = True
url = yarl.URL(stream_source)
try:
url = yarl.URL(stream_source)
except ValueError:
return {CONF_STREAM_SOURCE: "malformed_url"}
if not url.is_absolute():
return {CONF_STREAM_SOURCE: "relative_url"}
if not url.user and not url.password:
username = info.get(CONF_USERNAME)
password = info.get(CONF_PASSWORD)

View File

@ -6,6 +6,8 @@
"unable_still_load": "Unable to load valid image from still image URL (e.g. invalid host, URL or authentication failure). Review log for more info.",
"no_still_image_or_stream_url": "You must specify at least a still image or stream URL",
"invalid_still_image": "URL did not return a valid still image",
"malformed_url": "Malformed URL",
"relative_url": "Relative URLs are not allowed",
"stream_file_not_found": "File not found while trying to connect to stream (is ffmpeg installed?)",
"stream_http_not_found": "HTTP 404 Not found while trying to connect to stream",
"template_error": "Error rendering template. Review log for more info.",
@ -75,6 +77,8 @@
"unable_still_load": "[%key:component::generic::config::error::unable_still_load%]",
"no_still_image_or_stream_url": "[%key:component::generic::config::error::no_still_image_or_stream_url%]",
"invalid_still_image": "[%key:component::generic::config::error::invalid_still_image%]",
"malformed_url": "[%key:component::generic::config::error::malformed_url%]",
"relative_url": "[%key:component::generic::config::error::relative_url%]",
"template_error": "[%key:component::generic::config::error::template_error%]",
"timeout": "[%key:component::generic::config::error::timeout%]",
"stream_no_route_to_host": "[%key:component::generic::config::error::stream_no_route_to_host%]",

View File

@ -1,20 +1,19 @@
{
"config": {
"abort": {
"no_devices_found": "No devices found on the network",
"single_instance_allowed": "Already configured. Only a single configuration possible."
},
"error": {
"already_exists": "A camera with these URL settings already exists.",
"invalid_still_image": "URL did not return a valid still image",
"malformed_url": "Malformed URL",
"no_still_image_or_stream_url": "You must specify at least a still image or stream URL",
"relative_url": "Relative URLs are not allowed",
"stream_file_not_found": "File not found while trying to connect to stream (is ffmpeg installed?)",
"stream_http_not_found": "HTTP 404 Not found while trying to connect to stream",
"stream_io_error": "Input/Output error while trying to connect to stream. Wrong RTSP transport protocol?",
"stream_no_route_to_host": "Could not find host while trying to connect to stream",
"stream_no_video": "Stream has no video",
"stream_not_permitted": "Operation not permitted while trying to connect to stream. Wrong RTSP transport protocol?",
"stream_unauthorised": "Authorisation failed while trying to connect to stream",
"template_error": "Error rendering template. Review log for more info.",
"timeout": "Timeout while loading URL",
"unable_still_load": "Unable to load valid image from still image URL (e.g. invalid host, URL or authentication failure). Review log for more info.",
@ -50,14 +49,12 @@
"error": {
"already_exists": "A camera with these URL settings already exists.",
"invalid_still_image": "URL did not return a valid still image",
"malformed_url": "Malformed URL",
"no_still_image_or_stream_url": "You must specify at least a still image or stream URL",
"stream_file_not_found": "File not found while trying to connect to stream (is ffmpeg installed?)",
"stream_http_not_found": "HTTP 404 Not found while trying to connect to stream",
"relative_url": "Relative URLs are not allowed",
"stream_io_error": "Input/Output error while trying to connect to stream. Wrong RTSP transport protocol?",
"stream_no_route_to_host": "Could not find host while trying to connect to stream",
"stream_no_video": "Stream has no video",
"stream_not_permitted": "Operation not permitted while trying to connect to stream. Wrong RTSP transport protocol?",
"stream_unauthorised": "Authorisation failed while trying to connect to stream",
"template_error": "Error rendering template. Review log for more info.",
"timeout": "Timeout while loading URL",
"unable_still_load": "Unable to load valid image from still image URL (e.g. invalid host, URL or authentication failure). Review log for more info.",

View File

@ -180,33 +180,43 @@ async def test_form_only_still_sample(hass, user_flow, image_file):
@respx.mock
@pytest.mark.parametrize(
("template", "url", "expected_result"),
("template", "url", "expected_result", "expected_errors"),
[
# Test we can handle templates in strange parts of the url, #70961.
(
"http://localhost:812{{3}}/static/icons/favicon-apple-180x180.png",
"http://localhost:8123/static/icons/favicon-apple-180x180.png",
data_entry_flow.RESULT_TYPE_CREATE_ENTRY,
None,
),
(
"{% if 1 %}https://bla{% else %}https://yo{% endif %}",
"https://bla/",
data_entry_flow.RESULT_TYPE_CREATE_ENTRY,
None,
),
(
"http://{{example.org",
"http://example.org",
data_entry_flow.RESULT_TYPE_FORM,
{"still_image_url": "template_error"},
),
(
"invalid1://invalid:4\\1",
"invalid1://invalid:4%5c1",
data_entry_flow.RESULT_TYPE_CREATE_ENTRY,
data_entry_flow.RESULT_TYPE_FORM,
{"still_image_url": "malformed_url"},
),
(
"relative/urls/are/not/allowed.jpg",
"relative/urls/are/not/allowed.jpg",
data_entry_flow.RESULT_TYPE_FORM,
{"still_image_url": "relative_url"},
),
],
)
async def test_still_template(
hass, user_flow, fakeimgbytes_png, template, url, expected_result
hass, user_flow, fakeimgbytes_png, template, url, expected_result, expected_errors
) -> None:
"""Test we can handle various templates."""
respx.get(url).respond(stream=fakeimgbytes_png)
@ -220,6 +230,7 @@ async def test_still_template(
)
await hass.async_block_till_done()
assert result2["type"] == expected_result
assert result2.get("errors") == expected_errors
@respx.mock
@ -514,8 +525,29 @@ async def test_options_template_error(hass, fakeimgbytes_png, mock_create_stream
result4["flow_id"],
user_input=data,
)
assert result5.get("type") == data_entry_flow.RESULT_TYPE_FORM
assert result5["errors"] == {"stream_source": "template_error"}
assert result5.get("type") == data_entry_flow.RESULT_TYPE_FORM
assert result5["errors"] == {"stream_source": "template_error"}
# verify that an relative stream url is rejected.
data[CONF_STILL_IMAGE_URL] = "http://127.0.0.1/testurl/1"
data[CONF_STREAM_SOURCE] = "relative/stream.mjpeg"
result6 = await hass.config_entries.options.async_configure(
result5["flow_id"],
user_input=data,
)
assert result6.get("type") == data_entry_flow.RESULT_TYPE_FORM
assert result6["errors"] == {"stream_source": "relative_url"}
# verify that an malformed stream url is rejected.
data[CONF_STILL_IMAGE_URL] = "http://127.0.0.1/testurl/1"
data[CONF_STREAM_SOURCE] = "http://example.com:45:56"
result7 = await hass.config_entries.options.async_configure(
result6["flow_id"],
user_input=data,
)
assert result7.get("type") == data_entry_flow.RESULT_TYPE_FORM
assert result7["errors"] == {"stream_source": "malformed_url"}
async def test_slug(hass, caplog):
@ -528,6 +560,10 @@ async def test_slug(hass, caplog):
assert result is None
assert "Syntax error in" in caplog.text
result = slug(hass, "http://example.com:999999999999/stream")
assert result is None
assert "Syntax error in" in caplog.text
@respx.mock
async def test_options_only_stream(hass, fakeimgbytes_png, mock_create_stream):