From c5379a0f3533538a494874cd4a341397789fdeb1 Mon Sep 17 00:00:00 2001 From: Thomas Hollstegge Date: Mon, 4 May 2020 07:18:33 +0200 Subject: [PATCH] Improve emulated_hue compatibility with newer systems (#35148) * Make emulated hue detectable by Busch-Jaeger free@home SysAP * Emulated hue: Remove unnecessary host line from UPnP response * Test that external IPs are blocked for config route * Add another test for unauthorized users * Change hue username to nouser nouser seems to be used by the official Hue Bridge v1 Android app and is used by other projects as well Co-authored-by: J. Nick Koston --- .../components/emulated_hue/__init__.py | 2 + .../components/emulated_hue/hue_api.py | 35 +++++++++- homeassistant/components/emulated_hue/upnp.py | 10 +-- tests/components/emulated_hue/test_hue_api.py | 66 ++++++++++++++++++- tests/components/emulated_hue/test_upnp.py | 4 +- 5 files changed, 109 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/emulated_hue/__init__.py b/homeassistant/components/emulated_hue/__init__.py index da6e7acab40..5dbd52d09b1 100644 --- a/homeassistant/components/emulated_hue/__init__.py +++ b/homeassistant/components/emulated_hue/__init__.py @@ -14,6 +14,7 @@ from homeassistant.util.json import load_json, save_json from .hue_api import ( HueAllGroupsStateView, HueAllLightsStateView, + HueConfigView, HueFullStateView, HueGroupView, HueOneLightChangeView, @@ -119,6 +120,7 @@ async def async_setup(hass, yaml_config): HueAllGroupsStateView(config).register(app, app.router) HueGroupView(config).register(app, app.router) HueFullStateView(config).register(app, app.router) + HueConfigView(config).register(app, app.router) upnp_listener = UPNPResponderThread( config.host_ip_addr, diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py index d7830b7b699..069a4b60d0c 100644 --- a/homeassistant/components/emulated_hue/hue_api.py +++ b/homeassistant/components/emulated_hue/hue_api.py @@ -89,7 +89,7 @@ HUE_API_STATE_SAT_MAX = 254 HUE_API_STATE_CT_MIN = 153 # Color temp HUE_API_STATE_CT_MAX = 500 -HUE_API_USERNAME = "12345678901234567890" +HUE_API_USERNAME = "nouser" UNAUTHORIZED_USER = [ {"error": {"address": "/", "description": "unauthorized user", "type": "1"}} ] @@ -226,14 +226,47 @@ class HueFullStateView(HomeAssistantView): "config": { "mac": "00:00:00:00:00:00", "swversion": "01003542", + "apiversion": "1.17.0", "whitelist": {HUE_API_USERNAME: {"name": "HASS BRIDGE"}}, "ipaddress": f"{self.config.advertise_ip}:{self.config.advertise_port}", + "linkbutton": True, }, } return self.json(json_response) +class HueConfigView(HomeAssistantView): + """Return config view of emulated hue.""" + + url = "/api/{username}/config" + name = "emulated_hue:username:config" + requires_auth = False + + def __init__(self, config): + """Initialize the instance of the view.""" + self.config = config + + @core.callback + def get(self, request, username): + """Process a request to get the configuration.""" + if not is_local(request[KEY_REAL_IP]): + return self.json_message("only local IPs allowed", HTTP_UNAUTHORIZED) + if username != HUE_API_USERNAME: + return self.json(UNAUTHORIZED_USER) + + json_response = { + "mac": "00:00:00:00:00:00", + "swversion": "01003542", + "apiversion": "1.17.0", + "whitelist": {HUE_API_USERNAME: {"name": "HASS BRIDGE"}}, + "ipaddress": f"{self.config.advertise_ip}:{self.config.advertise_port}", + "linkbutton": True, + } + + return self.json(json_response) + + class HueOneLightStateView(HomeAssistantView): """Handle requests for getting info about a single entity.""" diff --git a/homeassistant/components/emulated_hue/upnp.py b/homeassistant/components/emulated_hue/upnp.py index c10fb3b826b..f0fe392f865 100644 --- a/homeassistant/components/emulated_hue/upnp.py +++ b/homeassistant/components/emulated_hue/upnp.py @@ -42,7 +42,7 @@ class DescriptionXmlView(HomeAssistantView): Philips hue bridge 2015 BSB002 http://www.meethue.com -1234 +001788FFFE23BFC2 uuid:2f402f80-da50-11e1-9b23-001788255acc @@ -77,10 +77,10 @@ class UPNPResponderThread(threading.Thread): CACHE-CONTROL: max-age=60 EXT: LOCATION: http://{advertise_ip}:{advertise_port}/description.xml -SERVER: FreeRTOS/6.0.5, UPnP/1.0, IpBridge/0.1 -hue-bridgeid: 1234 -ST: urn:schemas-upnp-org:device:basic:1 -USN: uuid:Socket-1_0-221438K0100073::urn:schemas-upnp-org:device:basic:1 +SERVER: FreeRTOS/6.0.5, UPnP/1.0, IpBridge/1.16.0 +hue-bridgeid: 001788FFFE23BFC2 +ST: upnp:rootdevice +USN: uuid:2f402f80-da50-11e1-9b23-00178829d301::upnp:rootdevice """ diff --git a/tests/components/emulated_hue/test_hue_api.py b/tests/components/emulated_hue/test_hue_api.py index 2171bac8c3f..fa97cd2f417 100644 --- a/tests/components/emulated_hue/test_hue_api.py +++ b/tests/components/emulated_hue/test_hue_api.py @@ -26,6 +26,7 @@ from homeassistant.components.emulated_hue.hue_api import ( HUE_API_USERNAME, HueAllGroupsStateView, HueAllLightsStateView, + HueConfigView, HueFullStateView, HueOneLightChangeView, HueOneLightStateView, @@ -191,6 +192,7 @@ def hue_client(loop, hass_hue, aiohttp_client): HueOneLightChangeView(config).register(web_app, web_app.router) HueAllGroupsStateView(config).register(web_app, web_app.router) HueFullStateView(config).register(web_app, web_app.router) + HueConfigView(config).register(web_app, web_app.router) return loop.run_until_complete(aiohttp_client(web_app)) @@ -330,7 +332,7 @@ async def test_discover_full_state(hue_client): # Make sure array is correct size assert len(result_json) == 2 - assert len(config_json) == 4 + assert len(config_json) == 6 assert len(lights_json) >= 1 # Make sure the config wrapper added to the config is there @@ -341,6 +343,10 @@ async def test_discover_full_state(hue_client): assert "swversion" in config_json assert "01003542" in config_json["swversion"] + # Make sure the api version is correct + assert "apiversion" in config_json + assert "1.17.0" in config_json["apiversion"] + # Make sure the correct username in config assert "whitelist" in config_json assert HUE_API_USERNAME in config_json["whitelist"] @@ -351,6 +357,49 @@ async def test_discover_full_state(hue_client): assert "ipaddress" in config_json assert "127.0.0.1:8300" in config_json["ipaddress"] + # Make sure the device announces a link button + assert "linkbutton" in config_json + assert config_json["linkbutton"] is True + + +async def test_discover_config(hue_client): + """Test the discovery of configuration.""" + result = await hue_client.get(f"/api/{HUE_API_USERNAME}/config") + + assert result.status == 200 + assert "application/json" in result.headers["content-type"] + + config_json = await result.json() + + # Make sure array is correct size + assert len(config_json) == 6 + + # Make sure the config wrapper added to the config is there + assert "mac" in config_json + assert "00:00:00:00:00:00" in config_json["mac"] + + # Make sure the correct version in config + assert "swversion" in config_json + assert "01003542" in config_json["swversion"] + + # Make sure the api version is correct + assert "apiversion" in config_json + assert "1.17.0" in config_json["apiversion"] + + # Make sure the correct username in config + assert "whitelist" in config_json + assert HUE_API_USERNAME in config_json["whitelist"] + assert "name" in config_json["whitelist"][HUE_API_USERNAME] + assert "HASS BRIDGE" in config_json["whitelist"][HUE_API_USERNAME]["name"] + + # Make sure the correct ip in config + assert "ipaddress" in config_json + assert "127.0.0.1:8300" in config_json["ipaddress"] + + # Make sure the device announces a link button + assert "linkbutton" in config_json + assert config_json["linkbutton"] is True + async def test_get_light_state(hass_hue, hue_client): """Test the getting of light state.""" @@ -905,6 +954,7 @@ async def test_external_ip_blocked(hue_client): getUrls = [ "/api/username/groups", "/api/username", + "/api/username/config", "/api/username/lights", "/api/username/lights/light.ceiling_lights", ] @@ -925,3 +975,17 @@ async def test_external_ip_blocked(hue_client): for putUrl in putUrls: result = await hue_client.put(putUrl) assert result.status == HTTP_UNAUTHORIZED + + +async def test_unauthorized_user_blocked(hue_client): + """Test unauthorized_user blocked.""" + getUrls = [ + "/api/wronguser", + "/api/wronguser/config", + ] + for getUrl in getUrls: + result = await hue_client.get(getUrl) + assert result.status == HTTP_OK + + result_json = await result.json() + assert result_json[0]["error"]["description"] == "unauthorized user" diff --git a/tests/components/emulated_hue/test_upnp.py b/tests/components/emulated_hue/test_upnp.py index 110ecf868e6..32859ca00c1 100644 --- a/tests/components/emulated_hue/test_upnp.py +++ b/tests/components/emulated_hue/test_upnp.py @@ -57,7 +57,9 @@ class TestEmulatedHue(unittest.TestCase): # Make sure the XML is parsable try: - ET.fromstring(result.text) + root = ET.fromstring(result.text) + ns = {"s": "urn:schemas-upnp-org:device-1-0"} + assert root.find("./s:device/s:serialNumber", ns).text == "001788FFFE23BFC2" except: # noqa: E722 pylint: disable=bare-except self.fail("description.xml is not valid XML!")