From 379ac36f452af6217938f68d422e96cd311454dc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 27 Jun 2022 03:49:58 -0700 Subject: [PATCH 1/6] Avoid parsing the url twice (#402) --- pyhap/hap_handler.py | 6 ++++-- tests/test_hap_handler.py | 3 +++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/pyhap/hap_handler.py b/pyhap/hap_handler.py index 22cd8e16..dd8131d5 100644 --- a/pyhap/hap_handler.py +++ b/pyhap/hap_handler.py @@ -145,6 +145,7 @@ def __init__(self, accessory_handler, client_address): self.command = None self.headers = None self.request_body = None + self.parsed_url = None self.response = None @@ -199,6 +200,7 @@ def dispatch(self, request, body=None): self.command = request.method.decode() self.headers = {k.decode(): v.decode() for k, v in request.headers} self.request_body = body + self.parsed_url = urlparse(self.path) response = HAPResponse() self.response = response @@ -210,7 +212,7 @@ def dispatch(self, request, body=None): self.headers, ) - path = urlparse(self.path).path + path = self.parsed_url.path try: getattr(self, self.HANDLERS[self.command][path])() except UnprivilegedRequestException: @@ -584,7 +586,7 @@ def handle_get_characteristics(self): raise UnprivilegedRequestException # Check that char exists and ... - params = parse_qs(urlparse(self.path).query) + params = parse_qs(self.parsed_url.query) response = self.accessory_handler.get_characteristics( params["id"][0].split(",") ) diff --git a/tests/test_hap_handler.py b/tests/test_hap_handler.py index 0225cfbd..bbc2fa7c 100644 --- a/tests/test_hap_handler.py +++ b/tests/test_hap_handler.py @@ -3,6 +3,7 @@ import json from unittest.mock import patch +from urllib.parse import urlparse from uuid import UUID import pytest @@ -697,6 +698,8 @@ def test_handle_get_characteristics_encrypted(driver): response = hap_handler.HAPResponse() handler.response = response handler.path = "/characteristics?id=1.11" + handler.parsed_url = urlparse(handler.path) + handler.handle_get_characteristics() assert response.status_code == 200 From 519f22fa72fd2ceff80b603c5813b2f7544b14eb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 27 Jun 2022 03:55:52 -0700 Subject: [PATCH 2/6] Switch to using ChaCha20Poly1305Reusable for encryption (#413) The ChaCha20Poly1305 that comes with cryptography recreates the ctx every time encrypt is called. Since we call encrypt in a loop, we can avoid this overhead by using ChaCha20Poly1305Reusable instead as we are doing everything in the same thread and not concerned about thread safety --- .github/workflows/ci.yaml | 2 +- docs/source/conf.py | 4 ++-- pyhap/accessory.py | 2 +- pyhap/camera.py | 4 ++-- pyhap/hap_crypto.py | 2 +- pyhap/hap_handler.py | 2 +- pyhap/hap_protocol.py | 2 +- pylintrc | 1 - setup.py | 2 +- tests/conftest.py | 2 +- tests/test_characteristic.py | 2 +- tests/test_hap_protocol.py | 2 +- tests/test_service.py | 4 +--- 13 files changed, 14 insertions(+), 17 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 579fa74b..40fbfe84 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.6, 3.7, 3.8, 3.9] + python-version: [3.7, 3.8, 3.9, "3.10"] steps: - uses: actions/checkout@v1 diff --git a/docs/source/conf.py b/docs/source/conf.py index 297b15b0..c0b3be16 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -60,7 +60,7 @@ # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. -language = None +#language = None # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. @@ -158,4 +158,4 @@ ] -# -- Extension configuration ------------------------------------------------- \ No newline at end of file +# -- Extension configuration ------------------------------------------------- diff --git a/pyhap/accessory.py b/pyhap/accessory.py index 083655e4..e8124fb7 100644 --- a/pyhap/accessory.py +++ b/pyhap/accessory.py @@ -245,7 +245,7 @@ def setup_message(self): ) else: print( - "To use the QR Code feature, use 'pip install " "HAP-python[QRCode]'", + "To use the QR Code feature, use 'pip install HAP-python[QRCode]'", flush=True, ) print( diff --git a/pyhap/camera.py b/pyhap/camera.py index c3b8e7a6..17002cfa 100644 --- a/pyhap/camera.py +++ b/pyhap/camera.py @@ -842,7 +842,7 @@ async def start_stream(self, session_info, stream_config): return True - async def stop_stream(self, session_info): # pylint: disable=no-self-use + async def stop_stream(self, session_info): """Stop the stream for the given ``session_id``. This method can be implemented if custom stop stream commands are needed. The @@ -886,7 +886,7 @@ async def reconfigure_stream(self, session_info, stream_config): """ await self.start_stream(session_info, stream_config) - def get_snapshot(self, image_size): # pylint: disable=unused-argument, no-self-use + def get_snapshot(self, image_size): # pylint: disable=unused-argument """Return a jpeg of a snapshot from the camera. Overwrite to implement getting snapshots from your camera. diff --git a/pyhap/hap_crypto.py b/pyhap/hap_crypto.py index 60e39e16..1ca64644 100644 --- a/pyhap/hap_crypto.py +++ b/pyhap/hap_crypto.py @@ -2,9 +2,9 @@ import logging import struct +from chacha20poly1305_reuseable import ChaCha20Poly1305Reusable as ChaCha20Poly1305 from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import hashes -from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305 from cryptography.hazmat.primitives.kdf.hkdf import HKDF logger = logging.getLogger(__name__) diff --git a/pyhap/hap_handler.py b/pyhap/hap_handler.py index dd8131d5..6c598077 100644 --- a/pyhap/hap_handler.py +++ b/pyhap/hap_handler.py @@ -12,7 +12,7 @@ from cryptography.exceptions import InvalidSignature, InvalidTag from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric import ed25519, x25519 -from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305 +from chacha20poly1305_reuseable import ChaCha20Poly1305Reusable as ChaCha20Poly1305 from pyhap import tlv from pyhap.const import ( diff --git a/pyhap/hap_protocol.py b/pyhap/hap_protocol.py index 33b750ab..597dfb5f 100644 --- a/pyhap/hap_protocol.py +++ b/pyhap/hap_protocol.py @@ -18,7 +18,7 @@ logger = logging.getLogger(__name__) -HIGH_WRITE_BUFFER_SIZE = 2 ** 19 +HIGH_WRITE_BUFFER_SIZE = 2**19 # We timeout idle connections after 90 hours as we must # clean up unused sockets periodically. 90 hours was choosen # as its the longest time we expect a user to be away from diff --git a/pylintrc b/pylintrc index b8eeab8e..032f9698 100644 --- a/pylintrc +++ b/pylintrc @@ -10,6 +10,5 @@ disable= too-many-public-methods, too-many-return-statements, too-many-statements, - bad-continuation, unused-argument, consider-using-with diff --git a/setup.py b/setup.py index 8b08f00f..24b356fd 100644 --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ README = f.read() -REQUIRES = ["cryptography", "zeroconf>=0.36.2", "h11"] +REQUIRES = ["cryptography", "chacha20poly1305-reuseable", "zeroconf>=0.36.2", "h11"] setup( diff --git a/tests/conftest.py b/tests/conftest.py index 20476b51..29d05c51 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -52,5 +52,5 @@ def __init__(self): def publish(self, data, client_addr=None, immediate=False): pass - def add_job(self, target, *args): # pylint: disable=no-self-use + def add_job(self, target, *args): asyncio.new_event_loop().run_until_complete(target(*args)) diff --git a/tests/test_characteristic.py b/tests/test_characteristic.py index 828e73f5..9faa25d6 100644 --- a/tests/test_characteristic.py +++ b/tests/test_characteristic.py @@ -45,7 +45,7 @@ def test_repr(): char = get_char(PROPERTIES.copy()) del char.properties["Permissions"] assert ( - char.__repr__() == "" ) diff --git a/tests/test_hap_protocol.py b/tests/test_hap_protocol.py index 80f875e9..0d2a58c3 100644 --- a/tests/test_hap_protocol.py +++ b/tests/test_hap_protocol.py @@ -28,7 +28,7 @@ def decrypt(self): self._crypt_in_buffer = bytearray() # Encrypted buffer return decrypted - def encrypt(self, data): # pylint: disable=no-self-use + def encrypt(self, data): """Mock as plaintext.""" return data diff --git a/tests/test_service.py b/tests/test_service.py index e17fc0c9..18a57624 100644 --- a/tests/test_service.py +++ b/tests/test_service.py @@ -30,9 +30,7 @@ def test_repr(): """Test service representation.""" service = Service(uuid1(), "TestService") service.characteristics = [get_chars()[0]] - assert ( - service.__repr__() == "" - ) + assert repr(service) == "" def test_add_characteristic(): From 03aec4657766961511be74d96a967afc0e3e91fd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 28 Jun 2022 03:52:14 -0700 Subject: [PATCH 3/6] Add support for orjson (#412) --- pyhap/hap_handler.py | 9 ++++----- pyhap/util.py | 14 +++++++++++--- requirements.txt | 2 ++ requirements_all.txt | 1 + setup.py | 2 +- 5 files changed, 19 insertions(+), 9 deletions(-) diff --git a/pyhap/hap_handler.py b/pyhap/hap_handler.py index 6c598077..6f4d7aed 100644 --- a/pyhap/hap_handler.py +++ b/pyhap/hap_handler.py @@ -4,7 +4,6 @@ """ import asyncio from http import HTTPStatus -import json import logging from urllib.parse import parse_qs, urlparse import uuid @@ -25,7 +24,7 @@ from pyhap.util import long_to_bytes from .hap_crypto import hap_hkdf, pad_tls_nonce -from .util import to_hap_json +from .util import to_hap_json, from_hap_json # iOS will terminate the connection if it does not respond within # 10 seconds, so we only allow 9 seconds to avoid this. @@ -614,7 +613,7 @@ def handle_set_characteristics(self): self.send_response(HTTPStatus.UNAUTHORIZED) return - requested_chars = json.loads(self.request_body.decode("utf-8")) + requested_chars = from_hap_json(self.request_body.decode("utf-8")) logger.debug( "%s: Set characteristics content: %s", self.client_address, requested_chars ) @@ -639,7 +638,7 @@ def handle_prepare(self): self.send_response(HTTPStatus.UNAUTHORIZED) return - request = json.loads(self.request_body.decode("utf-8")) + request = from_hap_json(self.request_body.decode("utf-8")) logger.debug("%s: prepare content: %s", self.client_address, request) response = self.accessory_handler.prepare(request, self.client_address) @@ -744,7 +743,7 @@ def _send_tlv_pairing_response(self, data): def handle_resource(self): """Get a snapshot from the camera.""" - data = json.loads(self.request_body.decode("utf-8")) + data = from_hap_json(self.request_body.decode("utf-8")) if self.accessory_handler.accessory.category == CATEGORY_BRIDGE: accessory = self.accessory_handler.accessory.accessories.get(data["aid"]) diff --git a/pyhap/util.py b/pyhap/util.py index 2f8f92f9..b8f9048c 100644 --- a/pyhap/util.py +++ b/pyhap/util.py @@ -1,11 +1,12 @@ import asyncio import base64 import functools -import json import random import socket from uuid import UUID +import orjson + from .const import BASE_UUID ALPHANUM = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" @@ -157,9 +158,16 @@ def hap_type_to_uuid(hap_type): def to_hap_json(dump_obj): """Convert an object to HAP json.""" - return json.dumps(dump_obj, separators=(",", ":")).encode("utf-8") + return orjson.dumps(dump_obj) # pylint: disable=no-member def to_sorted_hap_json(dump_obj): """Convert an object to sorted HAP json.""" - return json.dumps(dump_obj, sort_keys=True, separators=(",", ":")).encode("utf-8") + return orjson.dumps( # pylint: disable=no-member + dump_obj, option=orjson.OPT_SORT_KEYS # pylint: disable=no-member + ) + + +def from_hap_json(json_str): + """Convert json to an object.""" + return orjson.loads(json_str) # pylint: disable=no-member diff --git a/requirements.txt b/requirements.txt index 5b6a15d1..87ef3320 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,5 @@ h11 +chacha20poly1305-reuseable cryptography +orjson zeroconf diff --git a/requirements_all.txt b/requirements_all.txt index 41563ce4..89560833 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1,5 +1,6 @@ base36 cryptography +orjson pyqrcode h11 zeroconf diff --git a/setup.py b/setup.py index 24b356fd..1b70ddae 100644 --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ README = f.read() -REQUIRES = ["cryptography", "chacha20poly1305-reuseable", "zeroconf>=0.36.2", "h11"] +REQUIRES = ["cryptography", "chacha20poly1305-reuseable", "orjson>=3.7.2", "zeroconf>=0.36.2", "h11"] setup( From d536b27b1d2b2494251aea82a3aeb1c3f76a6d31 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 28 Jun 2022 03:53:23 -0700 Subject: [PATCH 4/6] Increase minimum python version to 3.7 (#417) --- pyhap/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyhap/const.py b/pyhap/const.py index b3509a06..d4cf9018 100644 --- a/pyhap/const.py +++ b/pyhap/const.py @@ -4,7 +4,7 @@ PATCH_VERSION = 0 __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" -REQUIRED_PYTHON_VER = (3, 6) +REQUIRED_PYTHON_VER = (3, 7) BASE_UUID = "-0000-1000-8000-0026BB765291" From 7a59fe85e4d1ece90859fa3eba08d4327215c93d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 28 Jun 2022 03:54:42 -0700 Subject: [PATCH 5/6] Speed up get accessories (#9) (#418) --- pyhap/characteristic.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pyhap/characteristic.py b/pyhap/characteristic.py index 02f85675..85461b56 100644 --- a/pyhap/characteristic.py +++ b/pyhap/characteristic.py @@ -82,7 +82,7 @@ PROP_UNIT = "unit" PROP_VALID_VALUES = "ValidValues" -PROP_NUMERIC = (PROP_MAX_VALUE, PROP_MIN_VALUE, PROP_MIN_STEP, PROP_UNIT) +PROP_NUMERIC = {PROP_MAX_VALUE, PROP_MIN_VALUE, PROP_MIN_STEP, PROP_UNIT} CHAR_BUTTON_EVENT = UUID("00000126-0000-1000-8000-0026BB765291") CHAR_PROGRAMMABLE_SWITCH_EVENT = UUID("00000073-0000-1000-8000-0026BB765291") @@ -358,7 +358,10 @@ def to_HAP(self): value = self.get_value() if self.properties[PROP_FORMAT] in HAP_FORMAT_NUMERICS: hap_rep.update( - {k: self.properties[k] for k in self.properties.keys() & PROP_NUMERIC} + { + k: self.properties[k] + for k in PROP_NUMERIC.intersection(self.properties) + } ) if PROP_VALID_VALUES in self.properties: From d9000899ab9af7452f6202fd831f4c4bb4d8d36e Mon Sep 17 00:00:00 2001 From: Ivan Kalchev Date: Tue, 28 Jun 2022 14:22:35 +0300 Subject: [PATCH 6/6] v4.5.0 --- CHANGELOG.md | 13 +++++++++++-- pyhap/const.py | 2 +- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fcc79a69..3dc67fda 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,16 +15,25 @@ Sections ### Breaking Changes ### Developers --> + +## [4.5.0] - 2022-06-28 + +- Speed up "get accessories". [#418](https://github.com/ikalchev/HAP-python/pull/418) +- Increase minimum python version to 3.7. [#417](https://github.com/ikalchev/HAP-python/pull/417) +- Speed up encryption by using ChaCha20Poly1305Reusable. [#413](https://github.com/ikalchev/HAP-python/pull/413) +- Speed up serialization using orjson. [#412](https://github.com/ikalchev/HAP-python/pull/412) +- Avoid redundant parsing of the URL. [#402](https://github.com/ikalchev/HAP-python/pull/402) + ## [4.4.0] - 2022-11-01 ### Added -- Allow invalid client values when enabled. [#392](https://github.com/ikalchev/HAP- python/pull/392) +- Allow invalid client values when enabled. [#392](https://github.com/ikalchev/HAP-python/pull/392) ## [4.3.0] - 2021-10-07 ### Fixed - Only send the latest state in case of multiple events for the same characteristic. [#385](https://github.com/ikalchev/HAP-python/pull/385) -- Handle invalid formats from clients. [#387](https://github.com/ikalchev/HAP- python/pull/387) +- Handle invalid formats from clients. [#387](https://github.com/ikalchev/HAP-python/pull/387) ## [4.2.1] - 2021-09-06 diff --git a/pyhap/const.py b/pyhap/const.py index d4cf9018..582659dd 100644 --- a/pyhap/const.py +++ b/pyhap/const.py @@ -1,6 +1,6 @@ """This module contains constants used by other modules.""" MAJOR_VERSION = 4 -MINOR_VERSION = 4 +MINOR_VERSION = 5 PATCH_VERSION = 0 __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}"