Beispiel #1
0
    def __init__(self, pairing_data):
        """

        :param pairing_data:
        :raises AccessoryNotFoundError: if the device can not be found via zeroconf
        """
        connected = False
        if 'AccessoryIP' in pairing_data and 'AccessoryPort' in pairing_data:
            # if it is known, try it
            accessory_ip = pairing_data['AccessoryIP']
            accessory_port = pairing_data['AccessoryPort']
            conn = HomeKitHTTPConnection(accessory_ip, port=accessory_port)
            try:
                conn.connect()
                c2a_key, a2c_key = get_session_keys(conn, pairing_data)
                connected = True
            except Exception as e:
                connected = False
        if not connected:
            # no connection yet, so ip / port might have changed and we need to fall back to slow zeroconf lookup
            device_id = pairing_data['AccessoryPairingID']
            connection_data = find_device_ip_and_port(device_id)
            if connection_data is None:
                raise AccessoryNotFoundError(
                    'Device {id} not found'.format(id=pairing_data['AccessoryPairingID']))
            conn = HomeKitHTTPConnection(connection_data['ip'], port=connection_data['port'])
            pairing_data['AccessoryIP'] = connection_data['ip']
            pairing_data['AccessoryPort'] = connection_data['port']
            c2a_key, a2c_key = get_session_keys(conn, pairing_data)

        self.sock = conn.sock
        self.c2a_key = c2a_key
        self.a2c_key = a2c_key
        self.pairing_data = pairing_data
        self.sec_http = SecureHttp(self)
    def test_negative_expected_length(self):
        # Regression test - in some versions of the secure http code 3 blocks of different lengths
        # could together trigger an attempt to read negative bytes from the socket.
        controller_socket, accessory_socket = socket.socketpair()

        key_c2a = b'S2}\xb1}-l\n\x83\xe5}\'U\xc0\x1b\x0f\x08%X\xfdu\x1f\x9el/\x9bZ"\xec5\xa5P'
        key_a2c = b'\x16\xab\xd3\xfe\x95{\xe56\x1fH\x81\xfd\x914\xa0@\xaa\x0e\xa6\xebw\xf2\xe3w:\x11/\x01\xbb;,\x1d'

        tthread = ResponseProvider(accessory_socket, key_c2a, key_a2c)
        tthread.data = [
            'HTTP/1.1 200 OK\r\nContent-Length: 1025\r\n\r\n', ' ' * 946,
            ' ' * 79
        ]
        tthread.start()

        with mock.patch(
                'homekit.controller.ip_implementation.IpSession') as session:
            session.sock = controller_socket
            session.a2c_key = key_a2c
            session.c2a_key = key_c2a
            session.pairing_data = {
                'AccessoryIP': '10.0.0.2',
                'AccessoryPort': 3000,
            }

            sh = SecureHttp(session, timeout=10)
            result = sh.get('/')

        controller_socket.close()
        accessory_socket.close()
        self.assertEqual(200, result.code)
        self.assertEqual(bytearray(b' ' * 1025), result.body)
    def test_get_on_connected_device(self):
        controller_socket, accessory_socket = socket.socketpair()

        key_c2a = b'S2}\xb1}-l\n\x83\xe5}\'U\xc0\x1b\x0f\x08%X\xfdu\x1f\x9el/\x9bZ"\xec5\xa5P'
        key_a2c = b'\x16\xab\xd3\xfe\x95{\xe56\x1fH\x81\xfd\x914\xa0@\xaa\x0e\xa6\xebw\xf2\xe3w:\x11/\x01\xbb;,\x1d'

        tthread = ResponseProvider(accessory_socket, key_c2a, key_a2c)
        tthread.start()

        with mock.patch(
                'homekit.controller.ip_implementation.IpSession') as session:
            session.sock = controller_socket
            session.a2c_key = key_a2c
            session.c2a_key = key_c2a
            session.pairing_data = {
                'AccessoryIP': '10.0.0.2',
                'AccessoryPort': 3000,
            }

            sh = SecureHttp(session, timeout=10)
            result = sh.get('/')

        controller_socket.close()
        accessory_socket.close()
        self.assertEqual(200, result.code)
        self.assertEqual(b'', result.body)
Beispiel #4
0
    def __init__(self, pairing_data):
        """

        :param pairing_data:
        :raises AccessoryNotFoundError: if the device can not be found via zeroconf
        """
        logging.debug('init session')
        connected = False
        self.pairing_data = pairing_data

        if 'AccessoryIP' in pairing_data and 'AccessoryPort' in pairing_data:
            # if it is known, try it
            accessory_ip = pairing_data['AccessoryIP']
            accessory_port = pairing_data['AccessoryPort']
            connected = self._connect(accessory_ip, accessory_port)

        if not connected:
            # no connection yet, so ip / port might have changed and we need to fall back to slow zeroconf lookup
            device_id = pairing_data['AccessoryPairingID']
            connection_data = find_device_ip_and_port(device_id)

            # update pairing data with the IP/port we elaborated above, perhaps next time they are valid
            pairing_data['AccessoryIP'] = connection_data['ip']
            pairing_data['AccessoryPort'] = connection_data['port']

            if connection_data is None:
                raise AccessoryNotFoundError(
                    'Device {id} not found'.format(id=pairing_data['AccessoryPairingID']))

            if not self._connect(connection_data['ip'], connection_data['port']):
                return

        logging.debug('session established')

        self.sec_http = SecureHttp(self)
Beispiel #5
0
    def test_requests_have_host_header(self):
        """
        The tado internet bridge will fail if a secure session request
        doesn't have a Host header.

        https://github.com/home-assistant/home-assistant/issues/16971
        https://github.com/jlusiardi/homekit_python/pull/130
        """

        session = mock.Mock()
        session.pairing_data = {
            'AccessoryIP': '192.168.1.2',
            'AccessoryPort': 8080,
        }
        secure_http = SecureHttp(session)

        with mock.patch.object(secure_http, '_handle_request') as handle_req:
            secure_http.get('/characteristics')
            assert '\r\nHost: 192.168.1.2:8080\r\n' in handle_req.call_args[0][
                0].decode()

            secure_http.post('/characteristics', b'')
            assert '\r\nHost: 192.168.1.2:8080\r\n' in handle_req.call_args[0][
                0].decode()

            secure_http.put('/characteristics', b'')
            assert '\r\nHost: 192.168.1.2:8080\r\n' in handle_req.call_args[0][
                0].decode()
    def test_get_on_connected_device_timeout(self):
        with mock.patch(
                'homekit.controller.ip_implementation.IpSession') as session:
            controller_socket, accessory_socket = socket.socketpair()

            session.sock = controller_socket

            session.a2c_key = b'\x00' * 32
            session.c2a_key = b'\x00' * 32
            session.pairing_data = {
                'AccessoryIP': '10.0.0.2',
                'AccessoryPort': 3000,
            }

            sh = SecureHttp(session, timeout=1)
            result = sh.get('/')
            controller_socket.close()
            accessory_socket.close()
            self.assertIsNone(result.code)
    def test_put_on_disconnected_device(self):
        with mock.patch(
                'homekit.controller.ip_implementation.IpSession') as session:
            session.sock = socket.socket()
            session.a2c_key = b'\x00' * 32
            session.c2a_key = b'\x00' * 32
            session.pairing_data = {
                'AccessoryIP': '10.0.0.2',
                'AccessoryPort': 3000,
            }

            sh = SecureHttp(session)
            self.assertRaises(AccessoryDisconnectedError, sh.put, '/', 'data')
Beispiel #8
0
class IpSession(object):
    def __init__(self, pairing_data):
        """

        :param pairing_data:
        :raises AccessoryNotFoundError: if the device can not be found via zeroconf
        """
        logging.debug('init session')
        connected = False
        if 'AccessoryIP' in pairing_data and 'AccessoryPort' in pairing_data:
            # if it is known, try it
            accessory_ip = pairing_data['AccessoryIP']
            accessory_port = pairing_data['AccessoryPort']
            conn = HomeKitHTTPConnection(accessory_ip, port=accessory_port)
            try:
                conn.connect()
                write_fun = create_ip_pair_verify_write(conn)
                c2a_key, a2c_key = get_session_keys(conn, pairing_data,
                                                    write_fun)
                connected = True
            except Exception:
                connected = False
        if not connected:
            # no connection yet, so ip / port might have changed and we need to fall back to slow zeroconf lookup
            device_id = pairing_data['AccessoryPairingID']
            connection_data = find_device_ip_and_port(device_id)
            if connection_data is None:
                raise AccessoryNotFoundError('Device {id} not found'.format(
                    id=pairing_data['AccessoryPairingID']))
            conn = HomeKitHTTPConnection(connection_data['ip'],
                                         port=connection_data['port'])
            pairing_data['AccessoryIP'] = connection_data['ip']
            pairing_data['AccessoryPort'] = connection_data['port']
            write_fun = create_ip_pair_verify_write(conn)
            c2a_key, a2c_key = get_session_keys(conn, pairing_data, write_fun)

        logging.debug('session established')
        self.sock = conn.sock
        self.c2a_key = c2a_key
        self.a2c_key = a2c_key
        self.pairing_data = pairing_data
        self.sec_http = SecureHttp(self)

    def close(self):
        """
        Close the session. This closes the socket.
        """
        try:
            self.sock.close()
        except OSError:
            # If we get an OSError its probably because the socket is already closed
            pass
        self.sock = None

    def get_from_pairing_data(self, key):
        if key not in self.pairing_data:
            return None
        return self.pairing_data[key]

    def set_in_pairing_data(self, key, value):
        self.pairing_data[key] = value

    def get(self, url):
        """
        Perform HTTP get via the encrypted session.
        :param url: The url to request
        :return: a homekit.http_impl.HttpResponse object
        """
        return self.sec_http.get(url)

    def put(self, url, body, content_type=HttpContentTypes.JSON):
        """
        Perform HTTP put via the encrypted session.
        :param url: The url to request
        :param body: the body of the put request
        :param content_type: the content of the content-type header
        :return: a homekit.http_impl.HttpResponse object
        """
        return self.sec_http.put(url, body, content_type)

    def post(self, url, body, content_type=HttpContentTypes.JSON):
        """
        Perform HTTP post via the encrypted session.
        :param url: The url to request
        :param body: the body of the post request
        :param content_type: the content of the content-type header
        :return: a homekit.http_impl.HttpResponse object
        """
        return self.sec_http.post(url, body, content_type)
class IpSession(object):
    def __init__(self, pairing_data):
        """

        :param pairing_data:
        :raises AccessoryNotFoundError: if the device can not be found via zeroconf
        """
        logging.debug('init session')
        connected = False
        self.pairing_data = pairing_data

        if 'AccessoryIP' in pairing_data and 'AccessoryPort' in pairing_data:
            # if it is known, try it
            accessory_ip = pairing_data['AccessoryIP']
            accessory_port = pairing_data['AccessoryPort']
            connected = self._connect(accessory_ip, accessory_port)

        if not connected:
            # no connection yet, so ip / port might have changed and we need to fall back to slow zeroconf lookup
            device_id = pairing_data['AccessoryPairingID']
            connection_data = find_device_ip_and_port(device_id)

            # update pairing data with the IP/port we elaborated above, perhaps next time they are valid
            pairing_data['AccessoryIP'] = connection_data['ip']
            pairing_data['AccessoryPort'] = connection_data['port']

            if connection_data is None:
                raise AccessoryNotFoundError('Device {id} not found'.format(
                    id=pairing_data['AccessoryPairingID']))

            if not self._connect(connection_data['ip'],
                                 connection_data['port']):
                return

        logging.debug('session established')

        self.sec_http = SecureHttp(self)

    def _connect(self, accessory_ip, accessory_port):
        conn = HomeKitHTTPConnection(accessory_ip, port=accessory_port)
        try:
            conn.connect()
            write_fun = create_ip_pair_verify_write(conn)

            state_machine = get_session_keys(self.pairing_data)

            request, expected = state_machine.send(None)
            while True:
                try:
                    response = write_fun(request, expected)
                    request, expected = state_machine.send(response)
                except StopIteration as result:
                    self.c2a_key, self.a2c_key = result.value
                    self.sock = conn.sock
                    return True
        except OSError as e:
            logging.debug("Failed to connect to accessory: %s", e.strerror)
        except Exception:
            logging.exception("Failed to connect to accessory")

        return False

    def close(self):
        """
        Close the session. This closes the socket.
        """
        try:
            self.sock.close()
        except OSError:
            # If we get an OSError its probably because the socket is already closed
            pass
        self.sock = None

    def get_from_pairing_data(self, key):
        if key not in self.pairing_data:
            return None
        return self.pairing_data[key]

    def set_in_pairing_data(self, key, value):
        self.pairing_data[key] = value

    def get(self, url):
        """
        Perform HTTP get via the encrypted session.
        :param url: The url to request
        :return: a homekit.http_impl.HttpResponse object
        """
        return self.sec_http.get(url)

    def put(self, url, body, content_type=HttpContentTypes.JSON):
        """
        Perform HTTP put via the encrypted session.
        :param url: The url to request
        :param body: the body of the put request
        :param content_type: the content of the content-type header
        :return: a homekit.http_impl.HttpResponse object
        """
        return self.sec_http.put(url, body, content_type)

    def post(self, url, body, content_type=HttpContentTypes.JSON):
        """
        Perform HTTP post via the encrypted session.
        :param url: The url to request
        :param body: the body of the post request
        :param content_type: the content of the content-type header
        :return: a homekit.http_impl.HttpResponse object
        """
        return self.sec_http.post(url, body, content_type)