Exemple #1
0
    async def request(self, method, target, headers=None, body=None):
        """
        Sends a HTTP request to the current transport and returns an awaitable
        that can be used to wait for the response.

        This will automatically set the header.

        :param method: A HTTP method, like 'GET' or 'POST'
        :param target: A URI to call the method on
        :param headers: a list of (header, value) tuples (optional)
        :param body: The body of the request (optional)
        """
        if not self.protocol:
            raise AccessoryDisconnectedError(
                "Connection lost before request could be sent"
            )

        buffer = []
        buffer.append(f"{method.upper()} {target} HTTP/1.1")

        # WARNING: It is vital that a Host: header is present or some devices
        # will reject the request.
        buffer.append(f"Host: {self.host}")

        if headers:
            for (header, value) in headers:
                buffer.append(f"{header}: {value}")

        buffer.append("")
        buffer.append("")

        # WARNING: We use \r\n explicitly. \n is not enough for some.
        request_bytes = "\r\n".join(buffer).encode("utf-8")

        if body:
            request_bytes += body

        # WARNING: It is vital that each request is sent in one call
        # Some devices are sensitive to unecrypted HTTP requests made in
        # multiple packets.

        # https://github.com/jlusiardi/homekit_python/issues/12
        # https://github.com/jlusiardi/homekit_python/issues/16

        async with self._concurrency_limit:
            if not self.protocol:
                raise AccessoryDisconnectedError("Tried to send while not connected")
            logger.debug("%s: raw request: %r", self.host, request_bytes)
            resp = await self.protocol.send_bytes(request_bytes)

        if resp.code >= 400 and resp.code <= 499:
            logger.debug(f"Got HTTP error {resp.code} for {method} against {target}")
            raise HttpErrorResponse(
                f"Got HTTP error {resp.code} for {method} against {target}",
                response=resp,
            )

        logger.debug("%s: raw response: %r", self.host, resp.body)

        return resp
Exemple #2
0
    async def put_json(self, target, body):
        response = await self.put(
            target,
            serialize_json(body),
            content_type=HttpContentTypes.JSON,
        )

        if response.code == 204:
            return {}

        try:
            decoded = response.body.decode("utf-8")
        except UnicodeDecodeError:
            self.transport.close()
            raise AccessoryDisconnectedError(
                "Session closed after receiving non-utf8 response"
            )

        try:
            parsed = hkjson.loads(decoded)
        except json.JSONDecodeError:
            self.transport.close()
            raise AccessoryDisconnectedError(
                "Session closed after receiving malformed response from device"
            )

        return parsed
Exemple #3
0
    async def _ensure_connected(self):
        try:
            await asyncio.wait_for(self.connection.ensure_connection(), 10)
        except asyncio.TimeoutError:
            raise AccessoryDisconnectedError(
                "Timeout while waiting for connection to device"
            )

        if not self.connection.is_connected:
            raise AccessoryDisconnectedError(
                "Ensure connection returned but still not connected"
            )
Exemple #4
0
    async def post_json(self, target, body):
        response = await self.post(
            target,
            serialize_json(body),
            content_type=HttpContentTypes.JSON,
        )

        if response.code != 204:
            # FIXME: ...
            pass

        decoded = response.body.decode("utf-8")

        if not decoded:
            # FIXME: Verify this is correct
            return {}

        try:
            parsed = hkjson.loads(decoded)
        except json.JSONDecodeError:
            self.transport.close()
            raise AccessoryDisconnectedError(
                "Session closed after receiving malformed response from device"
            )

        return parsed
Exemple #5
0
    async def send_bytes(self, payload):
        if self.transport.is_closing():
            # FIXME: It would be nice to try and wait for the reconnect in future.
            # In that case we need to make sure we do it at a layer above send_bytes otherwise
            # we might encrypt payloads with the last sessions keys then wait for a new connection
            # to send them - and on that connection the keys would be different.
            # Also need to make sure that the new connection has chance to pair-verify before
            # queued writes can happy.
            raise AccessoryDisconnectedError("Transport is closed")

        self.transport.write(payload)

        # We return a future so that our caller can block on a reply
        # We can send many requests and dispatch the results in order
        # Should mean we don't need locking around request/reply cycles
        result = asyncio.Future()
        self.result_cbs.append(result)

        try:
            return await asyncio.wait_for(result, 30)
        except asyncio.TimeoutError:
            self.transport.write_eof()
            self.transport.close()
            raise AccessoryDisconnectedError("Timeout while waiting for response")
Exemple #6
0
    async def _ensure_connected(self, *, _timeout: float = 10):
        try:
            try:
                await asyncio.wait_for(self.connection.ensure_connection(),
                                       _timeout)
            except asyncio.TimeoutError:
                raise AccessoryDisconnectedError(
                    "Timeout while waiting for connection to device")

            assert (
                self.connection.is_connected
            ), "must be connected or receive exception after waiting for connection attempt"
        except:  # noqa: E722  # Re-raise with cleanup is unproblematic
            # Ensure that no `_reconnect` task is left dangling in the background
            #
            # Previously, these would accumulate if the target device was
            # temporarily unavailable and cause a SYN-storm to hit the device
            # when it became available again.
            await self.connection.close()
            raise
Exemple #7
0
 def close(self):
     # If the connection is closed then any pending callbacks will never
     # fire, so set them to an error state.
     while self.result_cbs:
         result = self.result_cbs.pop(0)
         result.set_exception(AccessoryDisconnectedError("Connection closed"))