Skip to content

Commit

Permalink
Merge pull request #1 from jlusiardi/master
Browse files Browse the repository at this point in the history
Sync
  • Loading branch information
quarcko authored Feb 22, 2019
2 parents 61e792e + 202b28b commit bee2b17
Show file tree
Hide file tree
Showing 12 changed files with 275 additions and 58 deletions.
24 changes: 13 additions & 11 deletions homekit/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
from homekit.zeroconf_impl import discover_homekit_devices, find_device_ip_and_port
from homekit.protocol.statuscodes import HapStatusCodes
from homekit.exceptions import AccessoryNotFoundError, ConfigLoadingError, UnknownError, UnpairedError, \
AuthenticationError, ConfigSavingError, AlreadyPairedError, FormatError, AccessoryDisconnectedError
AuthenticationError, ConfigSavingError, AlreadyPairedError, FormatError, AccessoryDisconnectedError, \
EncryptionError
from homekit.http_impl.secure_http import SecureHttp
from homekit.protocol import get_session_keys, perform_pair_setup
from homekit.protocol.tlv import TLV, TlvParseException
Expand Down Expand Up @@ -59,7 +60,7 @@ def discover(max_seconds=10):
* md: the model name of the accessory (required)
* pv: the protocol version
* s#: the current state number (required)
* sf: the status flag (see table 5-9 page 70)
* sf / statusflags: the status flag (see table 5-9 page 70)
* ci/category: the category identifier in numerical and human readable form. For more information see table
12-3 page 254 or homekit.Categories (required)
Expand Down Expand Up @@ -133,11 +134,12 @@ def load_data(self, filename):
for pairing_id in data:
self.pairings[pairing_id] = Pairing(data[pairing_id])
except PermissionError as e:
raise ConfigLoadingError('Could not open "{f}" due to missing permissions'.format(f=filename))
raise ConfigLoadingError('Could not open "{f}" due to missing permissions.'.format(f=filename))
except JSONDecodeError as e:
raise ConfigLoadingError('Cannot parse "{f}" as JSON file'.format(f=filename))
raise ConfigLoadingError('Cannot parse "{f}" as JSON file.'.format(f=filename))
except FileNotFoundError as e:
raise ConfigLoadingError('Could not open "{f}" because it does not exist'.format(f=filename))
raise ConfigLoadingError('Could not open "{f}" because it does not exist. Use "python3 -m'
' homekit.init_controller_storage -f {f}" to initialize it.'.format(f=filename))

def save_data(self, filename):
"""
Expand Down Expand Up @@ -272,7 +274,7 @@ def list_accessories_and_characteristics(self):
self.session = Session(self.pairing_data)
try:
response = self.session.get('/accessories')
except AccessoryDisconnectedError:
except (AccessoryDisconnectedError, EncryptionError):
self.session.close()
self.session = None
raise
Expand Down Expand Up @@ -305,7 +307,7 @@ def list_pairings(self):
try:
response = self.session.sec_http.post('/pairings', request_tlv.decode())
data = response.read()
except AccessoryDisconnectedError:
except (AccessoryDisconnectedError, EncryptionError):
self.session.close()
self.session = None
raise
Expand Down Expand Up @@ -373,7 +375,7 @@ def get_characteristics(self, characteristics, include_meta=False, include_perms

try:
response = self.session.get(url)
except AccessoryDisconnectedError:
except (AccessoryDisconnectedError, EncryptionError):
self.session.close()
self.session = None
raise
Expand Down Expand Up @@ -437,7 +439,7 @@ def put_characteristics(self, characteristics, do_conversion=False):

try:
response = self.session.put('/characteristics', data)
except AccessoryDisconnectedError:
except (AccessoryDisconnectedError, EncryptionError):
self.session.close()
self.session = None
raise
Expand Down Expand Up @@ -490,7 +492,7 @@ def get_events(self, characteristics, callback_fun, max_events=-1, max_seconds=-

try:
response = self.session.put('/characteristics', data)
except AccessoryDisconnectedError:
except (AccessoryDisconnectedError, EncryptionError):
self.session.close()
self.session = None
raise
Expand Down Expand Up @@ -521,7 +523,7 @@ def get_events(self, characteristics, callback_fun, max_events=-1, max_seconds=-
try:
r = self.session.sec_http.handle_event_response()
body = r.read().decode()
except AccessoryDisconnectedError:
except (AccessoryDisconnectedError, EncryptionError):
self.session.close()
self.session = None
raise
Expand Down
2 changes: 1 addition & 1 deletion homekit/discover.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,6 @@ def setup_args_parser():
print('Model Name (md): {md}'.format(md=info['md']))
print('Protocol Version (pv): {pv}'.format(pv=info['pv']))
print('State Number (s#): {sn}'.format(sn=info['s#']))
print('Status Flags (sf): {sf}'.format(sf=info['sf']))
print('Status Flags (sf): {sf} (Flag: {flags})'.format(sf=info['statusflags'], flags=info['sf']))
print('Category Identifier (ci): {c} (Id: {ci})'.format(c=info['category'], ci=info['ci']))
print()
13 changes: 11 additions & 2 deletions homekit/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,11 +101,12 @@ class InvalidError(ProtocolError):
pass


class HttpException(HomeKitException):
class HttpException(Exception):
"""
Used within the HTTP Parser.
"""
pass
def __init__(self, message):
Exception.__init__(self, message)


class InvalidAuthTagError(ProtocolError):
Expand Down Expand Up @@ -163,6 +164,14 @@ def __init__(self, message):
Exception.__init__(self, message)


class EncryptionError(HomeKitException):
"""
Used if during a transmission some errors occurred.
"""
def __init__(self, message):
Exception.__init__(self, message)


class AccessoryDisconnectedError(HomeKitException):
"""
Used if a HomeKit disconnects part way through an operation or series of operations.
Expand Down
7 changes: 1 addition & 6 deletions homekit/http_impl/response.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
# limitations under the License.
#

from homekit.exceptions import HttpException

class HttpResponse(object):
STATE_PRE_STATUS = 0
Expand Down Expand Up @@ -120,9 +121,3 @@ def get_http_name(self):
if self.version is not None:
return self.version.split('/')[0]
return None


class HttpException(Exception):
def __init__(self):
pass

41 changes: 10 additions & 31 deletions homekit/http_impl/secure_http.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,22 +30,7 @@ class SecureHttp:
the HAP specification.
"""

class Wrapper:
def __init__(self, data):
self.data = data

def makefile(self, arg):
return io.BytesIO(self.data)

class HTTPResponseWrapper:
def __init__(self, data):
self.data = data
self.status = 200

def read(self):
return self.data

def __init__(self, session):
def __init__(self, session, timeout=10):
"""
Initializes the secure HTTP class. The required keys can be obtained with get_session_keys
Expand All @@ -58,6 +43,7 @@ def __init__(self, session):
self.c2a_key = session.c2a_key
self.c2a_counter = 0
self.a2c_counter = 0
self.timeout = timeout
self.lock = threading.Lock()

def get(self, target):
Expand Down Expand Up @@ -89,22 +75,10 @@ def _handle_request(self, data):

try:
self.sock.send(len_bytes + ciper_and_mac[0] + ciper_and_mac[1])
return self._read_response()
return self._read_response(self.timeout)
except OSError as e:
raise exceptions.AccessoryDisconnectedError(str(e))

@staticmethod
def _parse(chunked_data):
splitter = b'\r\n'
tmp = chunked_data.split(splitter, 1)
length = int(tmp[0].decode(), 16)
if length == 0:
return bytearray()

chunk = tmp[1][:length]
tmp[1] = tmp[1][length + 2:]
return chunk + SecureHttp._parse(tmp[1])

def _read_response(self, timeout=10):
# following the information from page 71 about HTTP Message splitting:
# The blocks start with 2 byte little endian defining the length of the encrypted data (max 1024 bytes)
Expand Down Expand Up @@ -154,9 +128,14 @@ def _read_response(self, timeout=10):
tmp = tmp[16:]

decrypted = self.decrypt_block(length, block, tag)
# TODO how to react to False?
if tmp is not False:
if decrypted is not False:
response.parse(decrypted)
else:
try:
self.sock.close()
except OSError:
pass
raise exceptions.EncryptionError('Error during transmission.')

# check how long next block will be
if int.from_bytes(tmp[0:2], 'little') < 1024:
Expand Down
38 changes: 38 additions & 0 deletions homekit/init_controller_storage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#!/usr/bin/env python3

#
# Copyright 2018 Joachim Lusiardi
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

import argparse
import os


def setup_args_parser():
parser = argparse.ArgumentParser(description='HomeKit initialize storage')
parser.add_argument('-f', action='store', required=True, dest='file', help='HomeKit pairing data file')
return parser.parse_args()


if __name__ == '__main__':
args = setup_args_parser()
if not os.path.isfile(args.file):
try:
with open(args.file, 'w') as fp:
fp.write('{}')
except PermissionError:
print('Permission denied to create file "{f}".'.format(f=args.file))
else:
print('File "{f}" already exists.'.format(f=args.file))
4 changes: 2 additions & 2 deletions homekit/model/feature_flags.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ class _FeatureFlags(object):

def __init__(self):
self._data = {
0: 'Paired',
1: 'Supports Pairing'
0: 'No support for HAP Pairing',
1: 'Supports HAP Pairing'
}

def __getitem__(self, item):
Expand Down
43 changes: 43 additions & 0 deletions homekit/model/status_flags.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#
# Copyright 2018 Joachim Lusiardi
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#


class _IpStatusFlags(object):
"""
Data taken form table 5-9 page 70
"""

def __getitem__(self, item):
i = int(item)
result = []
if i & 0x01:
result.append('Accessory has not been paired with any controllers.')
i = i - 0x01
else:
result.append('Accessory has been paired.')
if i & 0x02:
result.append('Accessory has not been configured to join a Wi-Fi network.')
i = i - 0x02
if i & 0x04:
result.append('A problem has been detected on the accessory.')
i = i - 0x04
if i == 0:
return ' '.join(result)
else:
raise KeyError('Item {item} not found'.format(item=item))


IpStatusFlags = _IpStatusFlags()
4 changes: 3 additions & 1 deletion homekit/zeroconf_impl/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

from homekit.model import Categories
from homekit.model.feature_flags import FeatureFlags
from homekit.model.status_flags import IpStatusFlags


class CollectingListener(object):
Expand Down Expand Up @@ -129,9 +130,10 @@ def discover_homekit_devices(max_seconds=10):
if s:
d['s#'] = s

sf = get_from_properties(props, b'sf', case_sensitive=False, default=0)
sf = get_from_properties(props, b'sf', case_sensitive=False)
if sf:
d['sf'] = sf
d['statusflags'] = IpStatusFlags[int(sf)]

ci = get_from_properties(props, b'ci', case_sensitive=False)
if ci:
Expand Down
2 changes: 1 addition & 1 deletion tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,4 @@
from tests.characteristicsTypes_test import TestCharacteristicsTypes
from tests.zeroconf_test import TestZeroconf
from tests.controller_test import TestControllerPaired, TestControllerUnpaired

from tests.secure_http_test import TestSecureHttp
Loading

0 comments on commit bee2b17

Please sign in to comment.