Skip to content

Commit d5f4041

Browse files
authored
Merge pull request #842 from JayNewstrom/webhook-request-kwarg
expose aiohttp request to @webhook_trigger functions
2 parents b9b7eb9 + a049c55 commit d5f4041

5 files changed

Lines changed: 57 additions & 1 deletion

File tree

custom_components/pyscript/decorators/webhook.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ async def _handler(_hass, webhook_id, request):
5454
func_args = {
5555
"trigger_type": "webhook",
5656
"webhook_id": webhook_id,
57+
"request": request,
5758
}
5859

5960
if "json" in request.headers.get(hdrs.CONTENT_TYPE, ""):

custom_components/pyscript/eval.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@
7878
"payload",
7979
"payload_obj",
8080
"qos",
81+
"request",
8182
"retain",
8283
"topic",
8384
"trigger_type",

custom_components/pyscript/stubs/pyscript_builtins.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,10 +133,12 @@ def webhook_trigger(
133133
134134
Args:
135135
webhook_id: Webhook id to listen to.
136-
str_expr: Optional expression evaluated against ``trigger_type``, ``webhook_id``, and ``payload``.
136+
str_expr: Optional expression evaluated against ``trigger_type``, ``webhook_id``, ``request``, and ``payload``.
137137
local_only: If False, allow requests from anywhere on the internet.
138138
methods: HTTP methods to allow.
139139
kwargs: Extra keyword arguments merged into each invocation.
140+
141+
Trigger kwargs include ``trigger_type="webhook"``, ``webhook_id``, the parsed payload fields, and ``request`` (the underlying ``aiohttp.web.Request``).
140142
"""
141143
...
142144

docs/reference.rst

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -882,6 +882,7 @@ variables:
882882
- ``trigger_type`` is set to "webhook"
883883
- ``webhook_id`` is set to the webhook_id that was called.
884884
- ``payload`` is the data/json that was sent in the request returned as a dictionary.
885+
- ``request`` is the underlying ``aiohttp.web.Request``. Use it to inspect headers (e.g. for HMAC signature validation), the HTTP method, query string, or to re-read the raw body via ``await request.read()`` (the body is cached after pyscript parses it into ``payload``).
885886

886887
When the ``@webhook_trigger`` occurs, those same variables are passed as keyword arguments to the function in case it needs them. Additional keyword parameters can be specified by setting the optional ``kwargs`` argument to a ``dict`` with the keywords and values.
887888

@@ -895,6 +896,25 @@ An simple example looks like
895896
896897
which if called using the curl command ``curl -X POST -d 'key1=xyz&key2=abc' hass_url/api/webhook/myid`` outputs ``It ran! {'key1': 'xyz', 'key2': 'abc'}, 10``
897898

899+
To validate an HMAC signature on incoming requests, declare ``request`` in the function and read the raw body:
900+
901+
.. code:: python
902+
903+
import hmac
904+
import hashlib
905+
906+
SECRET = b"shared-secret"
907+
908+
@webhook_trigger("github")
909+
def gh(payload, request):
910+
sig = request.headers.get("X-Hub-Signature-256", "")
911+
body = await request.read()
912+
expected = "sha256=" + hmac.new(SECRET, body, hashlib.sha256).hexdigest()
913+
if not hmac.compare_digest(sig, expected):
914+
log.warning("bad signature, ignoring")
915+
return
916+
log.info(f"verified webhook: {payload}")
917+
898918
NOTE: A webhook_id can only be used by either a built-in Home Assistant automation or pyscript, but not both. Trying to use the same webhook_id in both will result in an error.
899919

900920
@state_active

tests/test_decorators.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@
1010
from custom_components.pyscript import trigger
1111
from custom_components.pyscript.const import DOMAIN
1212
from custom_components.pyscript.function import Function
13+
from homeassistant.components import webhook
1314
from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_STATE_CHANGED
1415
from homeassistant.setup import async_setup_component
16+
from homeassistant.util.aiohttp import MockRequest
1517

1618

1719
async def setup_script(hass, notify_q, now, source):
@@ -224,3 +226,33 @@ def func6(value):
224226
hass.states.async_set("pyscript.var1", 6 + 2 * i)
225227
seq_num += 1
226228
assert literal_eval(await wait_until_done(notify_q)) == [seq_num, 6 + 2 * i]
229+
230+
231+
@pytest.mark.asyncio
232+
async def test_webhook_request_kwarg(hass):
233+
"""The aiohttp request is passed to the user function as the `request` kwarg."""
234+
notify_q = asyncio.Queue(0)
235+
await setup_script(
236+
hass,
237+
notify_q,
238+
[dt(2020, 7, 1, 11, 59, 59, 999999)],
239+
"""
240+
@webhook_trigger("test_req_hook")
241+
def webhook_test(payload, request):
242+
pyscript.done = [request.headers["X-My-Sig"], request.method, payload]
243+
""",
244+
)
245+
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
246+
await hass.async_block_till_done()
247+
248+
request = MockRequest(
249+
content=b'{"hello": "world"}',
250+
mock_source="test",
251+
method="POST",
252+
headers={"Content-Type": "application/json", "X-My-Sig": "abc123"},
253+
remote="127.0.0.1",
254+
)
255+
256+
await webhook.async_handle_webhook(hass, "test_req_hook", request)
257+
258+
assert literal_eval(await wait_until_done(notify_q)) == ["abc123", "POST", {"hello": "world"}]

0 commit comments

Comments
 (0)