def test_is_local(): """Test local addresses.""" assert network_util.is_local(ip_address("192.168.0.1")) assert network_util.is_local(ip_address("127.0.0.1")) assert network_util.is_local(ip_address("fd12:3456:789a:1::1")) assert network_util.is_local(ip_address("fe80::1234:5678:abcd")) assert network_util.is_local(ip_address("::ffff:192.168.0.1")) assert not network_util.is_local(ip_address("208.5.4.2")) assert not network_util.is_local(ip_address("198.51.100.1")) assert not network_util.is_local(ip_address("2001:DB8:FA1::1")) assert not network_util.is_local(ip_address("::ffff:208.5.4.2"))
def _get_deprecated_base_url( hass: HomeAssistant, *, internal: bool = False, allow_ip: bool = True, require_current_request: bool = False, require_ssl: bool = False, require_standard_port: bool = False, ) -> str: """Work with the deprecated `base_url`, used as fallback.""" if hass.config.api is None or not hass.config.api.deprecated_base_url: raise NoURLAvailableError base_url = yarl.URL(hass.config.api.deprecated_base_url) # Rules that apply to both internal and external if ((allow_ip or not is_ip_address(str(base_url.host))) and (not require_current_request or base_url.host == _get_request_host()) and (not require_ssl or base_url.scheme == "https") and (not require_standard_port or base_url.is_default_port())): # Check to ensure an internal URL if internal and (str(base_url.host).endswith(".local") or (is_ip_address(str(base_url.host)) and not is_loopback(ip_address(base_url.host)) and is_private(ip_address(base_url.host)))): return normalize_url(str(base_url)) # Check to ensure an external URL (a little) if (not internal and not str(base_url.host).endswith(".local") and not (is_ip_address(str(base_url.host)) and is_local(ip_address(str(base_url.host))))): return normalize_url(str(base_url)) raise NoURLAvailableError
async def get(self, request): """Handle a GET request.""" if not is_local(request[KEY_REAL_IP]): return self.json_message('only local IPs allowed', HTTP_BAD_REQUEST) return self.json([{'success': {'username': '******'}}])
async def async_step_user(self, user_input=None): """Handle a user initiated set up flow.""" if self._async_current_entries(): return self.async_abort(reason='one_instance_allowed') try: url_parts = urlparse(self.hass.config.api.base_url) if is_local(ip_address(url_parts.hostname)): return self.async_abort(reason='not_internet_accessible') except ValueError: # If it's not an IP address, it's very likely publicly accessible pass if user_input is None: return self.async_show_form( step_id='user', ) webhook_id = self.hass.components.webhook.async_generate_id() webhook_url = \ self.hass.components.webhook.async_generate_url(webhook_id) return self.async_create_entry( title='IFTTT Webhook', data={ CONF_WEBHOOK_ID: webhook_id }, description_placeholders={ 'applet_url': 'https://ifttt.com/maker_webhooks', 'webhook_url': webhook_url, 'docs_url': 'https://www.home-assistant.io/components/ifttt/' } )
def get(self, request, username, entity_id): """Process a request to get the state of an individual light.""" if not is_local(request[KEY_REAL_IP]): return self.json_message("only local IPs allowed", HTTP_BAD_REQUEST) hass = request.app["hass"] hass_entity_id = self.config.number_to_entity_id(entity_id) if hass_entity_id is None: _LOGGER.error( "Unknown entity number: %s not found in emulated_hue_ids.json", entity_id, ) return web.Response(text="Entity not found", status=404) entity = hass.states.get(hass_entity_id) if entity is None: _LOGGER.error("Entity not found: %s", hass_entity_id) return web.Response(text="Entity not found", status=404) if not self.config.is_entity_exposed(entity): _LOGGER.error("Entity not exposed: %s", entity_id) return web.Response(text="Entity not exposed", status=404) state = get_entity_state(self.config, entity) json_response = entity_to_json(self.config, entity, state) return self.json(json_response)
def get(self, request, username): """Process a request to get the list of available lights.""" if not is_local(ip_address(request.remote)): return self.json_message("Only local IPs allowed", HTTPStatus.UNAUTHORIZED) return self.json(create_list_of_entities(self.config, request))
async def async_step_user(self, user_input=None): """Handle a user initiated set up flow to create a webhook.""" if not self._allow_multiple and self._async_current_entries(): return self.async_abort(reason='one_instance_allowed') try: url_parts = urlparse(self.hass.config.api.base_url) if is_local(ip_address(url_parts.hostname)): return self.async_abort(reason='not_internet_accessible') except ValueError: # If it's not an IP address, it's very likely publicly accessible pass if user_input is None: return self.async_show_form( step_id='user', ) webhook_id = self.hass.components.webhook.async_generate_id() webhook_url = \ self.hass.components.webhook.async_generate_url(webhook_id) self._description_placeholder['webhook_url'] = webhook_url return self.async_create_entry( title=self._title, data={ 'webhook_id': webhook_id }, description_placeholders=self._description_placeholder )
def get(self, request, username): """Process a request to get the list of available lights.""" if not is_local(request[KEY_REAL_IP]): return self.json_message("Only local IPs allowed", HTTP_UNAUTHORIZED) return self.json(create_list_of_entities(self.config, request))
async def async_step_user(self, user_input=None): """Handle a user initiated set up flow to create a webhook.""" if not self._allow_multiple and self._async_current_entries(): return self.async_abort(reason='one_instance_allowed') try: url_parts = urlparse(self.hass.config.api.base_url) if is_local(ip_address(url_parts.hostname)): return self.async_abort(reason='not_internet_accessible') except ValueError: # If it's not an IP address, it's very likely publicly accessible pass if user_input is None: return self.async_show_form(step_id='user', ) webhook_id = self.hass.components.webhook.async_generate_id() webhook_url = \ self.hass.components.webhook.async_generate_url(webhook_id) self._description_placeholder['webhook_url'] = webhook_url return self.async_create_entry( title=self._title, data={'webhook_id': webhook_id}, description_placeholders=self._description_placeholder)
def get(self, request, username): """Process a request to get the list of available lights.""" 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 = { "lights": create_list_of_entities(self.config, request), "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)
def async_user_not_allowed_do_auth( hass: HomeAssistant, user: User, request: Request | None = None) -> str | None: """Validate that user is not allowed to do auth things.""" if not user.is_active: return "User is not active" if not user.local_only: return None # User is marked as local only, check if they are allowed to do auth if request is None: request = current_request.get() if not request: return "No request available to validate local access" if "cloud" in hass.config.components: # pylint: disable=import-outside-toplevel from hass_nabucasa import remote if remote.is_cloud_request.get(): return "User is local only" try: remote = ip_address(request.remote) except ValueError: return "Invalid remote IP" if is_local(remote): return None return "User cannot authenticate remotely"
def get(self, request, username, entity_id): """Process a request to get the state of an individual light.""" if not is_local(request[KEY_REAL_IP]): return self.json_message("Only local IPs allowed", HTTP_UNAUTHORIZED) hass = request.app["hass"] hass_entity_id = self.config.number_to_entity_id(entity_id) if hass_entity_id is None: _LOGGER.error( "Unknown entity number: %s not found in emulated_hue_ids.json", entity_id, ) return self.json_message("Entity not found", HTTP_NOT_FOUND) entity = hass.states.get(hass_entity_id) if entity is None: _LOGGER.error("Entity not found: %s", hass_entity_id) return self.json_message("Entity not found", HTTP_NOT_FOUND) if not self.config.is_entity_exposed(entity): _LOGGER.error("Entity not exposed: %s", entity_id) return self.json_message("Entity not exposed", HTTP_UNAUTHORIZED) json_response = entity_to_json(self.config, entity) return self.json(json_response)
def get(self, request, username): """Process a request to make the Brilliant Lightpad work.""" if not is_local(request[KEY_REAL_IP]): return self.json_message('only local IPs allowed', HTTP_BAD_REQUEST) return self.json({})
def get(self, request, username): """Process a request to make the Brilliant Lightpad work.""" if not is_local(ip_address(request.remote)): return self.json_message("Only local IPs allowed", HTTPStatus.UNAUTHORIZED) return self.json({})
def async_get_external_url(hass: HomeAssistant) -> Optional[str]: """Get external url of this instance. Note: currently it takes 30 seconds after Home Assistant starts for cloud.async_remote_ui_url to work. """ if "cloud" in hass.config.components: try: return cast(str, hass.components.cloud.async_remote_ui_url()) except hass.components.cloud.CloudNotAvailable: pass if hass.config.api is None: return None base_url = yarl.URL(hass.config.api.base_url) try: if is_local(ip_address(base_url.host)): return None except ValueError: # ip_address raises ValueError if host is not an IP address pass return str(base_url)
def get(self, request, username): """Process a request to make the Brilliant Lightpad work.""" if not is_local(request[KEY_REAL_IP]): return self.json_message("Only local IPs allowed", HTTP_UNAUTHORIZED) return self.json({})
def get(self, request, username=""): """Process a request to get the configuration.""" if not is_local(ip_address(request.remote)): return self.json_message("only local IPs allowed", HTTP_UNAUTHORIZED) json_response = create_config_model(self.config, request) return self.json(json_response)
def get(self, request: web.Request, username: str) -> web.Response: """Process a request to make the Brilliant Lightpad work.""" assert request.remote is not None if not is_local(ip_address(request.remote)): return self.json_message("Only local IPs allowed", HTTPStatus.UNAUTHORIZED) return self.json({})
def get(self, request, username): """Process a request to make the Brilliant Lightpad work.""" if not is_local(request[KEY_REAL_IP]): return self.json_message('only local IPs allowed', HTTP_BAD_REQUEST) return self.json({ })
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 = create_config_model(self.config, request) return self.json(json_response)
def get(self, request: web.Request, username: str = "") -> web.Response: """Process a request to get the configuration.""" assert request.remote is not None if not is_local(ip_address(request.remote)): return self.json_message("only local IPs allowed", HTTPStatus.UNAUTHORIZED) json_response = create_config_model(self.config, request) return self.json(json_response)
def put(self, request, username): """Process a request to make the Logitech Pop working.""" if not is_local(request[KEY_REAL_IP]): return self.json_message('only local IPs allowed', HTTP_BAD_REQUEST) return self.json([{ 'error': { 'address': '/groups/0/action/scene', 'type': 7, 'description': 'invalid value, dummy for parameter, scene' } }])
def put(self, request, username): """Process a request to make the Logitech Pop working.""" if not is_local(ip_address(request.remote)): return self.json_message("Only local IPs allowed", HTTPStatus.UNAUTHORIZED) return self.json([{ "error": { "address": "/groups/0/action/scene", "type": 7, "description": "invalid value, dummy for parameter, scene", } }])
def put(self, request, username): """Process a request to make the Logitech Pop working.""" if not is_local(request[KEY_REAL_IP]): return self.json_message('only local IPs allowed', HTTP_BAD_REQUEST) return self.json([{ 'error': { 'address': '/groups/0/action/scene', 'type': 7, 'description': 'invalid value, dummy for parameter, scene' } }])
def get(self, request, username): """Process a request to get the list of available lights.""" if not is_local(ip_address(request.remote)): return self.json_message("only local IPs allowed", HTTP_UNAUTHORIZED) if username != HUE_API_USERNAME: return self.json(UNAUTHORIZED_USER) json_response = { "lights": create_list_of_entities(self.config, request), "config": create_config_model(self.config, request), } return self.json(json_response)
def put(self, request, username): """Process a request to make the Logitech Pop working.""" if not is_local(request[KEY_REAL_IP]): return self.json_message("only local IPs allowed", HTTP_BAD_REQUEST) return self.json([{ "error": { "address": "/groups/0/action/scene", "type": 7, "description": "invalid value, dummy for parameter, scene", } }])
async def post(self, request): """Handle a POST request.""" if not is_local(ip_address(request.remote)): return self.json_message("Only local IPs allowed", HTTP_UNAUTHORIZED) try: data = await request.json() except ValueError: return self.json_message("Invalid JSON", HTTP_BAD_REQUEST) if "devicetype" not in data: return self.json_message("devicetype not specified", HTTP_BAD_REQUEST) return self.json([{"success": {"username": HUE_API_USERNAME}}])
async def post(self, request): """Handle a POST request.""" try: data = await request.json() except ValueError: return self.json_message("Invalid JSON", HTTP_BAD_REQUEST) if "devicetype" not in data: return self.json_message("devicetype not specified", HTTP_BAD_REQUEST) if not is_local(request[KEY_REAL_IP]): return self.json_message("only local IPs allowed", HTTP_BAD_REQUEST) return self.json([{"success": {"username": "******"}}])
def get(self, request, username): """Process a request to get the list of available lights.""" if not is_local(request[KEY_REAL_IP]): return self.json_message("Only local IPs allowed", HTTP_UNAUTHORIZED) hass = request.app["hass"] json_response = {} for entity in hass.states.async_all(): if self.config.is_entity_exposed(entity): number = self.config.entity_id_to_number(entity.entity_id) json_response[number] = entity_to_json(self.config, entity) return self.json(json_response)
async def put(self, request, username, entity_number): # noqa: C901 """Process a request to set the state of an individual light.""" if not is_local(ip_address(request.remote)): return self.json_message("Only local IPs allowed", HTTPStatus.UNAUTHORIZED) config = self.config hass = request.app["hass"] entity_id = config.number_to_entity_id(entity_number) if entity_id is None: _LOGGER.error("Unknown entity number: %s", entity_number) return self.json_message("Entity not found", HTTPStatus.NOT_FOUND) if (entity := hass.states.get(entity_id)) is None: _LOGGER.error("Entity not found: %s", entity_id) return self.json_message("Entity not found", HTTPStatus.NOT_FOUND)
async def post(self, request): """Handle a POST request.""" try: data = await request.json() except ValueError: return self.json_message('Invalid JSON', HTTP_BAD_REQUEST) if 'devicetype' not in data: return self.json_message('devicetype not specified', HTTP_BAD_REQUEST) if not is_local(request[KEY_REAL_IP]): return self.json_message('only local IPs allowed', HTTP_BAD_REQUEST) return self.json([{'success': {'username': '******'}}])
async def post(self, request): """Handle a POST request.""" try: data = await request.json() except ValueError: return self.json_message('Invalid JSON', HTTP_BAD_REQUEST) if 'devicetype' not in data: return self.json_message('devicetype not specified', HTTP_BAD_REQUEST) if not is_local(request[KEY_REAL_IP]): return self.json_message('only local IPs allowed', HTTP_BAD_REQUEST) return self.json([{'success': {'username': '******'}}])
def get(self, request, username): """Process a request to get the list of available lights.""" if not is_local(request[KEY_REAL_IP]): return self.json_message('only local IPs allowed', HTTP_BAD_REQUEST) hass = request.app['hass'] json_response = {} for entity in hass.states.async_all(): if self.config.is_entity_exposed(entity): state, brightness = get_entity_state(self.config, entity) number = self.config.entity_id_to_number(entity.entity_id) json_response[number] = entity_to_json(self.config, entity, state, brightness) return self.json(json_response)
def get(self, request, username): """Process a request to get the list of available lights.""" if not is_local(request[KEY_REAL_IP]): return self.json_message('only local IPs allowed', HTTP_BAD_REQUEST) hass = request.app['hass'] json_response = {} for entity in hass.states.async_all(): if self.config.is_entity_exposed(entity): state, brightness = get_entity_state(self.config, entity) number = self.config.entity_id_to_number(entity.entity_id) json_response[number] = entity_to_json( self.config, entity, state, brightness) return self.json(json_response)
def get(self, request, username, entity_id): """Process a request to get the state of an individual light.""" if not is_local(ip_address(request.remote)): return self.json_message("Only local IPs allowed", HTTPStatus.UNAUTHORIZED) hass = request.app["hass"] hass_entity_id = self.config.number_to_entity_id(entity_id) if hass_entity_id is None: _LOGGER.error( "Unknown entity number: %s not found in emulated_hue_ids.json", entity_id, ) return self.json_message("Entity not found", HTTPStatus.NOT_FOUND) if (entity := hass.states.get(hass_entity_id)) is None: _LOGGER.error("Entity not found: %s", hass_entity_id) return self.json_message("Entity not found", HTTPStatus.NOT_FOUND)
def get(self, request, username, entity_id): """Process a request to get the state of an individual light.""" if not is_local(request[KEY_REAL_IP]): return self.json_message('only local IPs allowed', HTTP_BAD_REQUEST) hass = request.app['hass'] entity_id = self.config.number_to_entity_id(entity_id) entity = hass.states.get(entity_id) if entity is None: _LOGGER.error('Entity not found: %s', entity_id) return web.Response(text="Entity not found", status=404) if not self.config.is_entity_exposed(entity): _LOGGER.error('Entity not exposed: %s', entity_id) return web.Response(text="Entity not exposed", status=404) state, brightness = get_entity_state(self.config, entity) json_response = entity_to_json(self.config, entity, state, brightness) return self.json(json_response)
def _parse_client_id(client_id): """Test if client id is a valid URL according to IndieAuth section 3.2. https://indieauth.spec.indieweb.org/#client-identifier """ parts = _parse_url(client_id) # Client identifier URLs # MUST have either an https or http scheme if parts.scheme not in ('http', 'https'): raise ValueError() # MUST contain a path component # Handled by url canonicalization. # MUST NOT contain single-dot or double-dot path segments if any(segment in ('.', '..') for segment in parts.path.split('/')): raise ValueError( 'Client ID cannot contain single-dot or double-dot path segments') # MUST NOT contain a fragment component if parts.fragment != '': raise ValueError('Client ID cannot contain a fragment') # MUST NOT contain a username or password component if parts.username is not None: raise ValueError('Client ID cannot contain username') if parts.password is not None: raise ValueError('Client ID cannot contain password') # MAY contain a port try: # parts raises ValueError when port cannot be parsed as int parts.port except ValueError: raise ValueError('Client ID contains invalid port') # Additionally, hostnames # MUST be domain names or a loopback interface and # MUST NOT be IPv4 or IPv6 addresses except for IPv4 127.0.0.1 # or IPv6 [::1] # We are not goint to follow the spec here. We are going to allow # any internal network IP to be used inside a client id. address = None try: netloc = parts.netloc # Strip the [, ] from ipv6 addresses before parsing if netloc[0] == '[' and netloc[-1] == ']': netloc = netloc[1:-1] address = ip_address(netloc) except ValueError: # Not an ip address pass if address is None or is_local(address): return parts raise ValueError('Hostname should be a domain name or local IP address')
async def put(self, request, username, entity_number): """Process a request to set the state of an individual light.""" if not is_local(request[KEY_REAL_IP]): return self.json_message('only local IPs allowed', HTTP_BAD_REQUEST) config = self.config hass = request.app['hass'] entity_id = config.number_to_entity_id(entity_number) if entity_id is None: _LOGGER.error('Unknown entity number: %s', entity_number) return self.json_message('Entity not found', HTTP_NOT_FOUND) entity = hass.states.get(entity_id) if entity is None: _LOGGER.error('Entity not found: %s', entity_id) return self.json_message('Entity not found', HTTP_NOT_FOUND) if not config.is_entity_exposed(entity): _LOGGER.error('Entity not exposed: %s', entity_id) return web.Response(text="Entity not exposed", status=404) try: request_json = await request.json() except ValueError: _LOGGER.error('Received invalid json') return self.json_message('Invalid JSON', HTTP_BAD_REQUEST) # Parse the request into requested "on" status and brightness parsed = parse_hue_api_put_light_body(request_json, entity) if parsed is None: _LOGGER.error('Unable to parse data: %s', request_json) return web.Response(text="Bad request", status=400) result, brightness = parsed # Choose general HA domain domain = core.DOMAIN # Entity needs separate call to turn on turn_on_needed = False # Convert the resulting "on" status into the service we need to call service = SERVICE_TURN_ON if result else SERVICE_TURN_OFF # Construct what we need to send to the service data = {ATTR_ENTITY_ID: entity_id} # Make sure the entity actually supports brightness entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) if entity.domain == light.DOMAIN: if entity_features & SUPPORT_BRIGHTNESS: if brightness is not None: data[ATTR_BRIGHTNESS] = brightness # If the requested entity is a script add some variables elif entity.domain == script.DOMAIN: data['variables'] = { 'requested_state': STATE_ON if result else STATE_OFF } if brightness is not None: data['variables']['requested_level'] = brightness # If the requested entity is a climate, set the temperature elif entity.domain == climate.DOMAIN: # We don't support turning climate devices on or off, # only setting the temperature service = None if entity_features & SUPPORT_TARGET_TEMPERATURE: if brightness is not None: domain = entity.domain service = SERVICE_SET_TEMPERATURE data[ATTR_TEMPERATURE] = brightness # If the requested entity is a media player, convert to volume elif entity.domain == media_player.DOMAIN: if entity_features & SUPPORT_VOLUME_SET: if brightness is not None: turn_on_needed = True domain = entity.domain service = SERVICE_VOLUME_SET # Convert 0-100 to 0.0-1.0 data[ATTR_MEDIA_VOLUME_LEVEL] = brightness / 100.0 # If the requested entity is a cover, convert to open_cover/close_cover elif entity.domain == cover.DOMAIN: domain = entity.domain if service == SERVICE_TURN_ON: service = SERVICE_OPEN_COVER else: service = SERVICE_CLOSE_COVER if entity_features & SUPPORT_SET_POSITION: if brightness is not None: domain = entity.domain service = SERVICE_SET_COVER_POSITION data[ATTR_POSITION] = brightness # If the requested entity is a fan, convert to speed elif entity.domain == fan.DOMAIN: if entity_features & SUPPORT_SET_SPEED: if brightness is not None: domain = entity.domain # Convert 0-100 to a fan speed if brightness == 0: data[ATTR_SPEED] = SPEED_OFF elif 0 < brightness <= 33.3: data[ATTR_SPEED] = SPEED_LOW elif 33.3 < brightness <= 66.6: data[ATTR_SPEED] = SPEED_MEDIUM elif 66.6 < brightness <= 100: data[ATTR_SPEED] = SPEED_HIGH if entity.domain in config.off_maps_to_on_domains: # Map the off command to on service = SERVICE_TURN_ON # Caching is required because things like scripts and scenes won't # report as "off" to Alexa if an "off" command is received, because # they'll map to "on". Thus, instead of reporting its actual # status, we report what Alexa will want to see, which is the same # as the actual requested command. config.cached_states[entity_id] = (result, brightness) # Separate call to turn on needed if turn_on_needed: hass.async_create_task(hass.services.async_call( core.DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id}, blocking=True)) if service is not None: hass.async_create_task(hass.services.async_call( domain, service, data, blocking=True)) json_response = \ [create_hue_success_response(entity_id, HUE_API_STATE_ON, result)] if brightness is not None: json_response.append(create_hue_success_response( entity_id, HUE_API_STATE_BRI, brightness)) return self.json(json_response)