async def _websocket_client(self): """Initialize a WebSocket API connection.""" url = f"{self.sys_homeassistant.api_url}/api/websocket" try: client = await self.sys_websession_ssl.ws_connect(url, heartbeat=60, verify_ssl=False) # Handle authentication data = await client.receive_json() if data.get('type') == 'auth_ok': return client if data.get('type') != 'auth_required': # Invalid protocol _LOGGER.error("Got unexpected response from HA WebSocket: %s", data) raise HTTPBadGateway() if self.sys_homeassistant.refresh_token: await self.sys_homeassistant.ensure_access_token() await client.send_json({ 'type': 'auth', 'access_token': self.sys_homeassistant.access_token, }) else: await client.send_json({ 'type': 'auth', 'api_password': self.sys_homeassistant.api_password, }) data = await client.receive_json() if data.get('type') == 'auth_ok': return client # Renew the Token is invalid if (data.get('type') == 'invalid_auth' and self.sys_homeassistant.refresh_token): self.sys_homeassistant.access_token = None return await self._websocket_client() raise HomeAssistantAuthError() except (RuntimeError, ValueError) as err: _LOGGER.error("Client error on WebSocket API %s.", err) except HomeAssistantAuthError as err: _LOGGER.error("Failed authentication to Home Assistant WebSocket") raise HTTPBadGateway()
def async_aiohttp_proxy_web(hass, request, web_coro, buffer_size=102400, timeout=10): """Stream websession request to aiohttp web response.""" try: with async_timeout.timeout(timeout, loop=hass.loop): req = yield from web_coro except asyncio.CancelledError: # The user cancelled the request return except asyncio.TimeoutError as err: # Timeout trying to start the web request raise HTTPGatewayTimeout() from err except aiohttp.ClientError as err: # Something went wrong with the connection raise HTTPBadGateway() from err try: yield from async_aiohttp_proxy_stream(hass, request, req.content, req.headers.get(CONTENT_TYPE)) finally: req.close()
async def _api_client(self, request: web.Request, path: str, timeout: int = 300): """Return a client request with proxy origin for Home Assistant.""" try: async with self.sys_homeassistant.make_request( request.method.lower(), f"api/{path}", headers={ name: value for name, value in request.headers.items() if name in FORWARD_HEADERS }, content_type=request.content_type, data=request.content, timeout=timeout, params=request.query, ) as resp: yield resp return except HomeAssistantAuthError: _LOGGER.error("Authenticate error on API for request %s", path) except HomeAssistantAPIError: _LOGGER.error("Error on API for request %s", path) except aiohttp.ClientError as err: _LOGGER.error("Client error on API %s request %s", path, err) except asyncio.TimeoutError: _LOGGER.error("Client timeout error on API request %s", path) raise HTTPBadGateway()
async def _websocket_client(self): """Initialize a websocket api connection.""" url = f"{self.sys_homeassistant.api_url}/api/websocket" try: client = await self.sys_websession_ssl.ws_connect(url, heartbeat=60, verify_ssl=False) # handle authentication for _ in range(2): data = await client.receive_json() if data.get('type') == 'auth_ok': return client elif data.get('type') == 'auth_required': await client.send_json({ 'type': 'auth', 'api_password': self.sys_homeassistant.api_password, }) _LOGGER.error("Authentication to Home-Assistant websocket") except (aiohttp.ClientError, RuntimeError) as err: _LOGGER.error("Client error on websocket API %s.", err) raise HTTPBadGateway()
async def handler( self, request: web.Request ) -> Union[web.Response, web.StreamResponse, web.WebSocketResponse]: """Route data to Supervisor ingress service.""" # Check Ingress Session session = request.cookies.get(COOKIE_INGRESS) if not self.sys_ingress.validate_session(session): _LOGGER.warning("No valid ingress session %s", session) raise HTTPUnauthorized() # Process requests addon = self._extract_addon(request) path = request.match_info.get("path") try: # Websocket if _is_websocket(request): return await self._handle_websocket(request, addon, path) # Request return await self._handle_request(request, addon, path) except aiohttp.ClientError as err: _LOGGER.error("Ingress error: %s", err) raise HTTPBadGateway()
def _command_proxy(self, path, request): """Return a client request with proxy origin for Hass.io supervisor. This method is a coroutine. """ read_timeout = _get_timeout(path) hass = request.app['hass'] try: data = None headers = {X_HASSIO: os.environ.get('HASSIO_TOKEN', "")} with async_timeout.timeout(10, loop=hass.loop): data = yield from request.read() if data: headers[CONTENT_TYPE] = request.content_type else: data = None method = getattr(self._websession, request.method.lower()) client = yield from method( "http://{}/{}".format(self._host, path), data=data, headers=headers, timeout=read_timeout ) return client except aiohttp.ClientError as err: _LOGGER.error("Client error on api %s request %s", path, err) except asyncio.TimeoutError: _LOGGER.error("Client timeout error on API request %s", path) raise HTTPBadGateway()
def get(self, request, addon): """Get addon data.""" data = yield from self.hassio.send_command( "/addons/{}/info".format(addon)) if not data: raise HTTPBadGateway() return web.json_response(data)
async def _handle( self, request: web.Request, token: str, path: str ) -> Union[web.Response, web.StreamResponse, web.WebSocketResponse]: """Route data to Hass.io ingress service.""" # validate token if token != self._valid_token: try: auth = self._hass.auth refresh_token = await auth.async_validate_access_token(token) if refresh_token is None: raise HTTPUnauthorized() from None # remember the token as valid self._valid_token = token except Exception: raise HTTPUnauthorized() from None try: # Websockettoken if _is_websocket(request): return await self._handle_websocket(request, token, path) # Request return await self._handle_request(request, token, path) except aiohttp.ClientError as err: _LOGGER.debug("Ingress error with %s / %s: %s", token, path, err) raise HTTPBadGateway() from None
async def _api_client(self, request, path, timeout=300): """Return a client request with proxy origin for Home Assistant.""" try: # read data with async_timeout.timeout(30): data = await request.read() if data: content_type = request.content_type else: content_type = None async with self.sys_homeassistant.make_request( request.method.lower(), f'api/{path}', content_type=content_type, data=data, timeout=timeout, ) as resp: yield resp return except HomeAssistantAuthError: _LOGGER.error("Authenticate error on API for request %s", path) except HomeAssistantAPIError: _LOGGER.error("Error on API for request %s", path) except aiohttp.ClientError as err: _LOGGER.error("Client error on API %s request %s", path, err) except asyncio.TimeoutError: _LOGGER.error("Client timeout error on API request %s", path) raise HTTPBadGateway()
def command_proxy(self, path, request): """Return a client request with proxy origin for HassIO supervisor. This method is a coroutine. """ try: data = None headers = None with async_timeout.timeout(TIMEOUT, loop=self.loop): data = yield from request.read() if data: headers = {CONTENT_TYPE: request.content_type} else: data = None method = getattr(self.websession, request.method.lower()) client = yield from method("http://{}/{}".format(self._ip, path), data=data, headers=headers) return client except aiohttp.ClientError as err: _LOGGER.error("Client error on api %s request %s.", path, err) except asyncio.TimeoutError: _LOGGER.error("Client timeout error on api request %s.", path) raise HTTPBadGateway()
async def async_aiohttp_proxy_web( hass: HomeAssistantType, request: web.BaseRequest, web_coro: Awaitable[aiohttp.ClientResponse], buffer_size: int = 102400, timeout: int = 10) -> Optional[web.StreamResponse]: """Stream websession request to aiohttp web response.""" try: with async_timeout.timeout(timeout, loop=hass.loop): req = await web_coro except asyncio.CancelledError: # The user cancelled the request return None except asyncio.TimeoutError as err: # Timeout trying to start the web request raise HTTPGatewayTimeout() from err except aiohttp.ClientError as err: # Something went wrong with the connection raise HTTPBadGateway() from err try: return await async_aiohttp_proxy_stream(hass, request, req.content, req.headers.get(CONTENT_TYPE)) finally: req.close()
async def _command_proxy( self, path: str, request: web.Request) -> web.Response | web.StreamResponse: """Return a client request with proxy origin for Hass.io supervisor. This method is a coroutine. """ read_timeout = _get_timeout(path) client_timeout = 10 data = None headers = _init_header(request) if path == "snapshots/new/upload": # We need to reuse the full content type that includes the boundary headers["Content-Type"] = request._stored_content_type # pylint: disable=protected-access # Snapshots are big, so we need to adjust the allowed size request._client_max_size = ( # pylint: disable=protected-access MAX_UPLOAD_SIZE) client_timeout = 300 try: with async_timeout.timeout(client_timeout): data = await request.read() method = getattr(self._websession, request.method.lower()) client = await method( f"http://{self._host}/{path}", data=data, headers=headers, timeout=read_timeout, ) # Simple request if int(client.headers.get(CONTENT_LENGTH, 0)) < 4194000: # Return Response body = await client.read() return web.Response(content_type=client.content_type, status=client.status, body=body) # Stream response response = web.StreamResponse(status=client.status, headers=client.headers) response.content_type = client.content_type await response.prepare(request) async for data in client.content.iter_chunked(4096): await response.write(data) return response except aiohttp.ClientError as err: _LOGGER.error("Client error on api %s request %s", path, err) except asyncio.TimeoutError: _LOGGER.error("Client timeout error on API request %s", path) raise HTTPBadGateway()
def post(self, request, addon): """Set options on host.""" data = yield from request.json() response = yield from self.hassio.send_raw( "/addons/{}/options".format(addon), payload=data) if not response: raise HTTPBadGateway() return web.json_response(response)
def post(self, request): """Set options on host.""" data = yield from request.json() response = yield from self.hassio.send_raw( self._url_options, payload=data) if not response: raise HTTPBadGateway() return web.json_response(response)
async def test_add_new_message_does_not_update_rounds_on_error(): round_mock = AsyncMock() round_mock.get_key = Mock(side_effect=HTTPBadGateway()) with patch("stummtaube.data.rounds.rounds", {567: Mock()}) as mocked_rounds: try: await add_new_message(round_mock, Mock()) assert False except HTTPBadGateway: assert mocked_rounds[567] is not None
async def _handle( self, request: web.Request, path: str ) -> Union[web.Response, web.StreamResponse, web.WebSocketResponse]: """Route data to service.""" try: return await self._handle_request(request, path) except aiohttp.ClientError as err: _LOGGER.debug("Reverse proxy error with %s: %s", path, err) raise HTTPBadGateway() from None
async def get( self, request: web.Request, **kwargs: Any, ) -> web.Response | web.StreamResponse | web.WebSocketResponse: """Route data to service.""" try: return await self._handle_request(request, **kwargs) except aiohttp.ClientError as err: _LOGGER.debug("Reverse proxy error for %s: %s", request.rel_url, err) raise HTTPBadGateway() from None
async def api(self, request: web.Request): """Proxy Home Assistant API Requests.""" self._check_access(request) if not await self.sys_homeassistant.check_api_state(): raise HTTPBadGateway() # Normal request path = request.match_info.get("path", "") async with self._api_client(request, path) as client: data = await client.read() return web.Response(body=data, status=client.status, content_type=client.content_type)
async def _command_proxy( self, path: str, request: web.Request) -> Union[web.Response, web.StreamResponse]: """Return a client request with proxy origin for Hass.io supervisor. This method is a coroutine. """ read_timeout = _get_timeout(path) hass = request.app['hass'] data = None headers = _init_header(request) try: with async_timeout.timeout(10, loop=hass.loop): data = await request.read() method = getattr(self._websession, request.method.lower()) client = await method("http://{}/{}".format(self._host, path), data=data, headers=headers, timeout=read_timeout) print(client.headers) # Simple request if int(client.headers.get(CONTENT_LENGTH, 0)) < 4194000: # Return Response body = await client.read() return web.Response( content_type=client.content_type, status=client.status, body=body, ) # Stream response response = web.StreamResponse(status=client.status) response.content_type = client.content_type await response.prepare(request) async for data in client.content.iter_chunked(4096): await response.write(data) return response except aiohttp.ClientError as err: _LOGGER.error("Client error on api %s request %s", path, err) except asyncio.TimeoutError: _LOGGER.error("Client timeout error on API request %s", path) raise HTTPBadGateway()
async def _handle( self, request: web.Request, addon: str, path: str ) -> Union[web.Response, web.StreamResponse, web.WebSocketResponse]: """Route data to Hass.io ingress service.""" try: # Websocket if _is_websocket(request): return await self._handle_websocket(request, addon, path) # Request return await self._handle_request(request, addon, path) except aiohttp.ClientError: pass raise HTTPBadGateway() from None
async def _handle( self, request: web.Request, token: str, path: str ) -> web.Response | web.StreamResponse | web.WebSocketResponse: """Route data to Hass.io ingress service.""" try: # Websocket if _is_websocket(request): return await self._handle_websocket(request, token, path) # Request return await self._handle_request(request, token, path) except aiohttp.ClientError as err: _LOGGER.debug("Ingress error with %s / %s: %s", token, path, err) raise HTTPBadGateway() from None
async def _command_proxy( self, path: str, request: web.Request ) -> web.StreamResponse: """Return a client request with proxy origin for Hass.io supervisor. This method is a coroutine. """ headers = _init_header(request) if path == "backups/new/upload": # We need to reuse the full content type that includes the boundary headers[ "Content-Type" ] = request._stored_content_type # pylint: disable=protected-access try: client = await self._websession.request( method=request.method, url=f"http://{self._host}/{path}", params=request.query, data=request.content, headers=headers, timeout=_get_timeout(path), ) # Stream response response = web.StreamResponse( status=client.status, headers=_response_header(client, path) ) response.content_type = client.content_type await response.prepare(request) async for data in client.content.iter_chunked(4096): await response.write(data) return response except aiohttp.ClientError as err: _LOGGER.error("Client error on api %s request %s", path, err) except asyncio.TimeoutError: _LOGGER.error("Client timeout error on API request %s", path) raise HTTPBadGateway()
async def _api_client(self, request, path, timeout=300): """Return a client request with proxy origin for Home-Assistant.""" url = f"{self.sys_homeassistant.api_url}/api/{path}" try: data = None headers = {} method = getattr(self._websession_ssl, request.method.lower()) params = request.query or None # read data with async_timeout.timeout(30): data = await request.read() if data: headers.update({CONTENT_TYPE: request.content_type}) # need api password? if self.sys_homeassistant.api_password: headers = { HEADER_HA_ACCESS: self.sys_homeassistant.api_password, } # reset headers if not headers: headers = None client = await method(url, data=data, headers=headers, timeout=timeout, params=params) return client except aiohttp.ClientError as err: _LOGGER.error("Client error on API %s request %s.", path, err) except asyncio.TimeoutError: _LOGGER.error("Client timeout error on API request %s.", path) raise HTTPBadGateway()
async def stream(self, request: web.Request): """Proxy HomeAssistant EventStream Requests.""" self._check_access(request) if not await self.sys_homeassistant.check_api_state(): raise HTTPBadGateway() _LOGGER.info("Home Assistant EventStream start") async with self._api_client(request, "stream", timeout=None) as client: response = web.StreamResponse() response.content_type = request.headers.get(CONTENT_TYPE) try: await response.prepare(request) async for data in client.content: await response.write(data) except (aiohttp.ClientError, aiohttp.ClientPayloadError): pass _LOGGER.info("Home Assistant EventStream close") return response
async def _handle( self, request: web.Request, path: str ) -> Union[web.Response, web.StreamResponse, web.WebSocketResponse]: """Check the authentication""" # authenticated = request.get(KEY_AUTHENTICATED, False) # _LOGGER.warning("request " + str(request)) # if not authenticated: # return web.Response(status=401) """Route data to ingress service.""" try: # Websocket if _is_websocket(request): return await self._handle_websocket(request, path) # Request # if not request["ha_authenticated"]: # return web.Response(status=401) return await self._handle_request(request, path) except aiohttp.ClientError as err: _LOGGER.debug("Ingress error with %s: %s", path, err) raise HTTPBadGateway() from None
async def homeassistant_proxy(self, path, request): """Return a client request with proxy origin for Home-Assistant.""" url = "{}/api/{}".format(self.homeassistant.api_url, path) try: data = None headers = {} method = getattr(self.homeassistant.websession, request.method.lower()) # read data with async_timeout.timeout(10, loop=self.loop): data = await request.read() if data: headers.update({CONTENT_TYPE: request.content_type}) # need api password? if self.homeassistant.api_password: headers = {HEADER_HA_ACCESS: self.homeassistant.api_password} # reset headers if not headers: headers = None client = await method(url, data=data, headers=headers, timeout=300) return client except aiohttp.ClientError as err: _LOGGER.error("Client error on api %s request %s.", path, err) except asyncio.TimeoutError: _LOGGER.error("Client timeout error on api request %s.", path) raise HTTPBadGateway()
def get(self, request): """Get base data.""" data = yield from self.hassio.send_command(self._url_info) if not data: raise HTTPBadGateway() return web.json_response(data)
async def websocket(self, request: web.Request): """Initialize a WebSocket API connection.""" if not await self.sys_homeassistant.check_api_state(): raise HTTPBadGateway() _LOGGER.info("Home Assistant WebSocket API request initialize") # init server server = web.WebSocketResponse(heartbeat=30) await server.prepare(request) # handle authentication try: await server.send_json({ "type": "auth_required", "ha_version": self.sys_homeassistant.version }) # Check API access response = await server.receive_json() supervisor_token = response.get("api_password") or response.get( "access_token") addon = self.sys_addons.from_token(supervisor_token) if not addon or not addon.access_homeassistant_api: _LOGGER.warning("Unauthorized WebSocket access!") await server.send_json({ "type": "auth_invalid", "message": "Invalid access" }) return server _LOGGER.info("WebSocket access from %s", addon.slug) await server.send_json({ "type": "auth_ok", "ha_version": self.sys_homeassistant.version }) except (RuntimeError, ValueError) as err: _LOGGER.error("Can't initialize handshake: %s", err) return server # init connection to hass try: client = await self._websocket_client() except APIError: return server _LOGGER.info("Home Assistant WebSocket API request running") try: client_read = None server_read = None while not server.closed and not client.closed: if not client_read: client_read = self.sys_create_task(client.receive_str()) if not server_read: server_read = self.sys_create_task(server.receive_str()) # wait until data need to be processed await asyncio.wait([client_read, server_read], return_when=asyncio.FIRST_COMPLETED) # server if server_read.done() and not client.closed: server_read.exception() await client.send_str(server_read.result()) server_read = None # client if client_read.done() and not server.closed: client_read.exception() await server.send_str(client_read.result()) client_read = None except asyncio.CancelledError: pass except (RuntimeError, ConnectionError, TypeError) as err: _LOGGER.info("Home Assistant WebSocket API error: %s", err) finally: if client_read: client_read.cancel() if server_read: server_read.cancel() # close connections if not client.closed: await client.close() if not server.closed: await server.close() _LOGGER.info("Home Assistant WebSocket API connection is closed") return server