Skip to content

Commit

Permalink
Merge pull request #461 from ikalchev/v4.8.0
Browse files Browse the repository at this point in the history
V4.8.0
  • Loading branch information
ikalchev authored Oct 6, 2023
2 parents 5f45a5e + 8b32d97 commit e281b36
Show file tree
Hide file tree
Showing 18 changed files with 555 additions and 67 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.7, 3.8, 3.9, "3.10", "3.11"]
python-version: [3.7, 3.8, 3.9, "3.10", "3.11", "3.12"]

steps:
- uses: actions/checkout@v1
Expand Down
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,15 @@ Sections
### Developers
-->

## [4.8.0] - 2023-10-06

- Add AccessoryInformation:HardwareFinish and NFCAccess characteristics/services.
[#454](https://github.com/ikalchev/HAP-python/pull/454)
- Fix handling of multiple pairings. [#456](https://github.com/ikalchev/HAP-python/pull/456)
- Save raw client username bytes if they are missing on successful pair verify.[#458](https://github.com/ikalchev/HAP-python/pull/458)
- Add support for Write Responses. [#459](https://github.com/ikalchev/HAP-python/pull/459)
- Ensure tasks are not garbage-collected before they finish. [#460](https://github.com/ikalchev/HAP-python/pull/460)

## [4.7.1] - 2023-07-31

- Improve encryption performance. [#448](https://github.com/ikalchev/HAP-python/pull/448)
Expand Down
37 changes: 30 additions & 7 deletions pyhap/accessory_driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
HAP_REPR_STATUS,
HAP_REPR_TTL,
HAP_REPR_VALUE,
HAP_REPR_WRITE_RESPONSE,
STANDALONE_AID,
)
from pyhap.encoder import AccessoryEncoder
Expand All @@ -71,16 +72,16 @@
def _wrap_char_setter(char, value, client_addr):
"""Process an characteristic setter callback trapping and logging all exceptions."""
try:
char.client_update_value(value, client_addr)
result = char.client_update_value(value, client_addr)
except Exception: # pylint: disable=broad-except
logger.exception(
"%s: Error while setting characteristic %s to %s",
client_addr,
char.display_name,
value,
)
return HAP_SERVER_STATUS.SERVICE_COMMUNICATION_FAILURE
return HAP_SERVER_STATUS.SUCCESS
return HAP_SERVER_STATUS.SERVICE_COMMUNICATION_FAILURE, None
return HAP_SERVER_STATUS.SUCCESS, result


def _wrap_acc_setter(acc, updates_by_service, client_addr):
Expand Down Expand Up @@ -615,7 +616,7 @@ def async_update_advertisement(self):
self.mdns_service_info = AccessoryMDNSServiceInfo(
self.accessory, self.state, self.zeroconf_server
)
asyncio.ensure_future(
util.async_create_background_task(
self.advertiser.async_update_service(self.mdns_service_info)
)

Expand All @@ -627,7 +628,7 @@ def async_persist(self):
"""
loop = asyncio.get_event_loop()
logger.debug("Scheduling write of accessory state to disk")
asyncio.ensure_future(loop.run_in_executor(None, self.persist))
util.async_create_background_task(loop.run_in_executor(None, self.persist))

def persist(self):
"""Saves the state of the accessory.
Expand Down Expand Up @@ -851,6 +852,7 @@ def set_characteristics(self, chars_query, client_addr):
"iid": 2,
"value": False, # Value to set
"ev": True # (Un)subscribe for events from this characteristics.
"r": True # Request write response
}]
}
Expand All @@ -859,7 +861,9 @@ def set_characteristics(self, chars_query, client_addr):
# TODO: Add support for chars that do no support notifications.
updates = {}
setter_results = {}
setter_responses = {}
had_error = False
had_write_response = False
expired = False

if HAP_REPR_PID in chars_query:
Expand All @@ -872,6 +876,10 @@ def set_characteristics(self, chars_query, client_addr):
aid, iid = cq[HAP_REPR_AID], cq[HAP_REPR_IID]
setter_results.setdefault(aid, {})

if HAP_REPR_WRITE_RESPONSE in cq:
setter_responses.setdefault(aid, {})
had_write_response = True

if expired:
setter_results[aid][iid] = HAP_SERVER_STATUS.INVALID_VALUE_IN_REQUEST
had_error = True
Expand Down Expand Up @@ -904,11 +912,21 @@ def set_characteristics(self, chars_query, client_addr):
# Characteristic level setter callbacks
char = acc.get_characteristic(aid, iid)

set_result = _wrap_char_setter(char, value, client_addr)
set_result, set_result_value = _wrap_char_setter(char, value, client_addr)
if set_result != HAP_SERVER_STATUS.SUCCESS:
had_error = True

setter_results[aid][iid] = set_result

if set_result_value is not None:
if setter_responses.get(aid, None) is None:
logger.warning(
"Returning write response '%s' when it wasn't requested for %s %s",
set_result_value, aid, iid
)
had_write_response = True
setter_responses.setdefault(aid, {})[iid] = set_result_value

if not char.service or (
not acc.setter_callback and not char.service.setter_callback
):
Expand All @@ -934,7 +952,7 @@ def set_characteristics(self, chars_query, client_addr):
for char in chars:
setter_results[aid][char_to_iid[char]] = set_result

if not had_error:
if not had_error and not had_write_response:
return None

return {
Expand All @@ -943,6 +961,11 @@ def set_characteristics(self, chars_query, client_addr):
HAP_REPR_AID: aid,
HAP_REPR_IID: iid,
HAP_REPR_STATUS: status,
**(
{HAP_REPR_VALUE: setter_responses[aid][iid]}
if setter_responses.get(aid, {}).get(iid, None) is not None
else {}
)
}
for aid, iid_status in setter_results.items()
for iid, status in iid_status.items()
Expand Down
4 changes: 3 additions & 1 deletion pyhap/characteristic.py
Original file line number Diff line number Diff line change
Expand Up @@ -321,14 +321,16 @@ def client_update_value(self, value, sender_client_addr=None):
)
previous_value = self.value
self.value = value
response = None
if self.setter_callback:
# pylint: disable=not-callable
self.setter_callback(value)
response = self.setter_callback(value)
changed = self.value != previous_value
if changed:
self.notify(sender_client_addr)
if self.type_id in ALWAYS_NULL:
self.value = None
return response

def notify(self, sender_client_addr=None):
"""Notify clients about a value change. Sends the value.
Expand Down
5 changes: 3 additions & 2 deletions pyhap/const.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""This module contains constants used by other modules."""
MAJOR_VERSION = 4
MINOR_VERSION = 7
PATCH_VERSION = 1
MINOR_VERSION = 8
PATCH_VERSION = 0
__short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__ = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER = (3, 7)
Expand Down Expand Up @@ -78,6 +78,7 @@
HAP_REPR_TYPE = "type"
HAP_REPR_VALUE = "value"
HAP_REPR_VALID_VALUES = "valid-values"
HAP_REPR_WRITE_RESPONSE = "r"

HAP_PROTOCOL_VERSION = "01.01.00"
HAP_PROTOCOL_SHORT_VERSION = "1.1"
Expand Down
43 changes: 29 additions & 14 deletions pyhap/hap_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import asyncio
from http import HTTPStatus
import logging
from typing import TYPE_CHECKING, Dict, Optional
from typing import TYPE_CHECKING, Dict, Optional, Any
from urllib.parse import ParseResult, parse_qs, urlparse
import uuid

Expand Down Expand Up @@ -88,6 +88,7 @@ class HAP_TLV_TAGS:
ERROR_CODE = b"\x07"
PROOF = b"\x0A"
PERMISSIONS = b"\x0B"
SEPARATOR = b"\xFF"


class UnprivilegedRequestException(Exception):
Expand Down Expand Up @@ -148,7 +149,7 @@ def __init__(self, accessory_handler, client_address):
"""
self.accessory_handler: AccessoryDriver = accessory_handler
self.state: State = self.accessory_handler.state
self.enc_context = None
self.enc_context: Optional[Dict[str, Any]] = None
self.client_address = client_address
self.is_encrypted = False
self.client_uuid: Optional[uuid.UUID] = None
Expand Down Expand Up @@ -567,33 +568,33 @@ def _pair_verify_two(self, tlv_objects: Dict[bytes, bytes]) -> None:

dec_tlv_objects = tlv.decode(bytes(decrypted_data))
client_username = dec_tlv_objects[HAP_TLV_TAGS.USERNAME]
material = (
self.enc_context["client_public"]
+ client_username
+ self.enc_context["public_key"].public_bytes(
encoding=serialization.Encoding.Raw,
format=serialization.PublicFormat.Raw,
)
public_key: x25519.X25519PublicKey = self.enc_context["public_key"]
raw_public_key = public_key.public_bytes(
encoding=serialization.Encoding.Raw,
format=serialization.PublicFormat.Raw,
)
material = self.enc_context["client_public"] + client_username + raw_public_key

client_uuid = uuid.UUID(str(client_username, "utf-8"))
perm_client_public = self.state.paired_clients.get(client_uuid)
if perm_client_public is None:
logger.error(
"%s: Client %s with uuid %s attempted pair verify without being paired first (paired clients=%s).",
"%s: Client %s with uuid %s attempted pair verify "
"without being paired first (public_key=%s, paired clients=%s).",
self.accessory_handler.accessory.display_name,
self.client_address,
client_uuid,
self.state.paired_clients,
self.accessory_handler.accessory.display_name,
raw_public_key.hex(),
{uuid: key.hex() for uuid, key in self.state.paired_clients.items()},
)
self._send_authentication_error_tlv_response(HAP_TLV_STATES.M4)
return

verifying_key = ed25519.Ed25519PublicKey.from_public_bytes(perm_client_public)
try:
verifying_key.verify(dec_tlv_objects[HAP_TLV_TAGS.PROOF], material)
except InvalidSignature:
logger.error("%s: Bad signature, abort.", self.client_address)
except (InvalidSignature, KeyError) as ex:
logger.error("%s: %s, abort.", self.client_address, ex)
self._send_authentication_error_tlv_response(HAP_TLV_STATES.M4)
return

Expand All @@ -605,6 +606,13 @@ def _pair_verify_two(self, tlv_objects: Dict[bytes, bytes]) -> None:

data = tlv.encode(HAP_TLV_TAGS.SEQUENCE_NUM, HAP_TLV_STATES.M4)
self._send_tlv_pairing_response(data)

if client_uuid not in self.state.uuid_to_bytes:
# We are missing the raw bytes for this client, so we need to
# add them to the state and persist so list pairings works.
self.state.uuid_to_bytes[client_uuid] = client_username
self.accessory_handler.async_persist()

assert self.response is not None # nosec
self.response.shared_key = self.enc_context["shared_key"]
self.is_encrypted = True
Expand Down Expand Up @@ -781,9 +789,16 @@ def _handle_list_pairings(self) -> None:
client_public,
HAP_TLV_TAGS.PERMISSIONS,
HAP_PERMISSIONS.ADMIN if admin else HAP_PERMISSIONS.USER,
HAP_TLV_TAGS.SEPARATOR,
b"",
]
)

if response[-2] == HAP_TLV_TAGS.SEPARATOR:
# The last pairing should not have a separator
response.pop()
response.pop()

data = tlv.encode(*response)
self._send_tlv_pairing_response(data)

Expand Down
3 changes: 2 additions & 1 deletion pyhap/hap_protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from .hap_crypto import HAPCrypto
from .hap_event import create_hap_event
from .hap_handler import HAPResponse, HAPServerHandler
from .util import async_create_background_task

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -270,7 +271,7 @@ def _process_response(self, response) -> None:
self.hap_crypto = HAPCrypto(response.shared_key)
# Only update mDNS after sending the response
if response.pairing_changed:
asyncio.ensure_future(
async_create_background_task(
self.loop.run_in_executor(None, self.accessory_driver.finish_pair)
)

Expand Down
1 change: 0 additions & 1 deletion pyhap/params.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@


def get_srp_context(ng_group_len, hashfunc, salt_len=16, secret_len=32):

group = _ng_const[ng_order.index(ng_group_len)]

ctx = {
Expand Down
31 changes: 31 additions & 0 deletions pyhap/resources/characteristics.json
Original file line number Diff line number Diff line change
Expand Up @@ -1664,5 +1664,36 @@
"maxValue": 100,
"minValue": 0,
"unit": "percentage"
},
"HardwareFinish": {
"Format": "tlv8",
"Permissions": [
"pr"
],
"UUID": "0000026C-0000-1000-8000-0026BB765291"
},
"ConfigurationState": {
"Format": "uint16",
"Permissions": [
"pr",
"ev"
],
"UUID": "00000263-0000-1000-8000-0026BB765291"
},
"NFCAccessControlPoint": {
"Format": "tlv8",
"Permissions": [
"pr",
"pw",
"wr"
],
"UUID": "00000264-0000-1000-8000-0026BB765291"
},
"NFCAccessSupportedConfiguration": {
"Format": "tlv8",
"Permissions": [
"pr"
],
"UUID": "00000265-0000-1000-8000-0026BB765291"
}
}
12 changes: 11 additions & 1 deletion pyhap/resources/services.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
"AccessoryInformation": {
"OptionalCharacteristics": [
"HardwareRevision",
"AccessoryFlags"
"AccessoryFlags",
"HardwareFinish"
],
"RequiredCharacteristics": [
"Identify",
Expand Down Expand Up @@ -576,5 +577,14 @@
"PositionState"
],
"UUID": "0000008C-0000-1000-8000-0026BB765291"
},
"NFCAccess": {
"OptionalCharacteristics": [],
"RequiredCharacteristics": [
"ConfigurationState",
"NFCAccessControlPoint",
"NFCAccessSupportedConfiguration"
],
"UUID": "00000266-0000-1000-8000-0026BB765291"
}
}
4 changes: 2 additions & 2 deletions pyhap/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,13 @@ def __init__(
self.addresses = address
else:
self.addresses = [util.get_local_address()]
self.mac = mac or util.generate_mac()
self.mac: str = mac or util.generate_mac()
self.pincode = pincode or util.generate_pincode()
self.port = port or DEFAULT_PORT
self.setup_id = util.generate_setup_id()

self.config_version = DEFAULT_CONFIG_VERSION
self.paired_clients = {}
self.paired_clients: Dict[UUID, bytes] = {}
self.client_properties = {}

self.private_key = ed25519.Ed25519PrivateKey.generate()
Expand Down
Loading

0 comments on commit e281b36

Please sign in to comment.