Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add experimental support for WebPush #177

Merged
merged 50 commits into from
Mar 17, 2021
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
febbe6d
first draft of webpush pushkin
bwindels Mar 11, 2021
5aa5e96
declare dependencies
bwindels Mar 11, 2021
2cba009
precompute the claims
bwindels Mar 12, 2021
657bacf
allow client to pass session_id for multiple pushers by 1 service worker
bwindels Mar 12, 2021
4dc8e60
apply style suggestion
bwindels Mar 12, 2021
dee1b1f
point to commit hash for fork of pywebpush and add comment why needed
bwindels Mar 12, 2021
b04db8f
english
bwindels Mar 12, 2021
dc904cb
build payload more cautiously, and adopt default_payload like gcm/aspn
bwindels Mar 12, 2021
6d326e7
always send content if present, as the payload is encrypted
bwindels Mar 15, 2021
6f50a86
always send the counts if they are set, even 0
bwindels Mar 15, 2021
f262ed2
check config in ctor
bwindels Mar 15, 2021
c4acf9d
update fork commit hash after PR feedback
bwindels Mar 15, 2021
7ad9133
copy GCM connection handling
bwindels Mar 15, 2021
0e233f2
add documentation for pusher
bwindels Mar 15, 2021
4325f15
update docs
bwindels Mar 15, 2021
34ab6b8
add changelog file
bwindels Mar 15, 2021
cda7fda
fix code style in pushkin
bwindels Mar 15, 2021
8002e5a
fix setup.py code style errors
bwindels Mar 15, 2021
35e0a0e
wrong extension for changelog file
bwindels Mar 15, 2021
538442c
more codestyle fixes
bwindels Mar 15, 2021
eba9c80
reformat to make linter happy
bwindels Mar 15, 2021
3198745
import sorting
bwindels Mar 15, 2021
f81c686
the linter seems to want a newline here ¯\_(ツ)_/¯
bwindels Mar 15, 2021
4115809
upstream PR got merged, update git dependency
bwindels Mar 16, 2021
33ccde8
Merge branch 'master' into bwindels/webpush
bwindels Mar 16, 2021
83def21
ignore type checking for py_vapid and pywebpush
bwindels Mar 16, 2021
8685f2c
2nd attempt to ignore type checking for py_vapid and pywebpush
bwindels Mar 16, 2021
ab65309
3rd attempt to ignore type checking for py_vapid and pywebpush
bwindels Mar 16, 2021
8ded12f
remove git dep, merged upstream PR got released
bwindels Mar 17, 2021
9e3cad8
validate push data better, and reject pushkey if invalid
bwindels Mar 17, 2021
40f5a40
remove comment
bwindels Mar 17, 2021
7531a29
Update sygnal/webpushpushkin.py
bwindels Mar 17, 2021
74d7244
Update sygnal/webpushpushkin.py
bwindels Mar 17, 2021
299363f
wrap any exception from loading vapid priv key at startup
bwindels Mar 17, 2021
5d5cb38
clean up docs
bwindels Mar 17, 2021
10fe061
counts, not count
bwindels Mar 17, 2021
0669242
clean up logging
bwindels Mar 17, 2021
8f0303a
provide docstrings for http wrapper
bwindels Mar 17, 2021
6887687
status code 200 is probably more correct/future-proof here
bwindels Mar 17, 2021
ae5e530
remove obsolete logging
bwindels Mar 17, 2021
e28f783
add docstring for post method parameters
bwindels Mar 17, 2021
0abb95d
black-ify formatting
bwindels Mar 17, 2021
ec88640
remove trailing space
bwindels Mar 17, 2021
5403fca
typo
bwindels Mar 17, 2021
ed7d038
Re-work docstrings.
clokep Mar 17, 2021
65915db
Update some of the documentation.
clokep Mar 17, 2021
4011ad3
Update changelog.
clokep Mar 17, 2021
ee43211
Black-ify
clokep Mar 17, 2021
9ad5991
Clarify documentation.
clokep Mar 17, 2021
96cd23a
Catch a more specific exception.
clokep Mar 17, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ def read(fname):
"idna>=2.8",
"psycopg2>=2.8.4",
"importlib_metadata",
# a minor change was needed to make this library work with async io, which has been proposed
# to merge upstream at https://github.com/web-push-libs/pywebpush/pull/133
# for now, we point to the fork
"pywebpush@git+https://github.com/matrix-org/pywebpush#b723ef616eaf7cd79e592a9627d601b202017572"
"py-vapid>=1.7.0"
],
long_description=read("README.rst"),
)
167 changes: 167 additions & 0 deletions sygnal/webpushpushkin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
# -*- coding: utf-8 -*-
# Copyright 2021 The Matrix.org Foundation C.I.C.
#
# 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 json
import logging
import time
from io import BytesIO
from json import JSONDecodeError

from pywebpush import webpush, WebPushException
from py_vapid import Vapid
from opentracing import logs, tags
from prometheus_client import Counter, Gauge, Histogram
from twisted.enterprise.adbapi import ConnectionPool
from twisted.internet.defer import DeferredSemaphore
from twisted.web.client import FileBodyProducer, HTTPConnectionPool, readBody
from twisted.web.http_headers import Headers

from sygnal.exceptions import (
NotificationDispatchException,
TemporaryNotificationDispatchException,
)
from sygnal.helper.context_factory import ClientTLSOptionsFactory
from sygnal.helper.proxy.proxyagent_twisted import ProxyAgent
from sygnal.utils import NotificationLoggerAdapter, twisted_sleep

from .exceptions import PushkinSetupException
from .notifications import ConcurrencyLimitedPushkin

logger = logging.getLogger(__name__)

MAX_TRIES = 3
RETRY_DELAY_BASE = 10
MAX_BYTES_PER_FIELD = 1024

DEFAULT_MAX_CONNECTIONS = 20


class WebpushPushkin(ConcurrencyLimitedPushkin):
"""
Pushkin that relays notifications to Google/Firebase Cloud Messaging.
"""

UNDERSTOOD_CONFIG_FIELDS = {
"type",
"max_connections",
"vapid_private_key",
"vapid_contact_email",
} | ConcurrencyLimitedPushkin.UNDERSTOOD_CONFIG_FIELDS

def __init__(self, name, sygnal, config):
super(WebpushPushkin, self).__init__(name, sygnal, config)

nonunderstood = self.cfg.keys() - self.UNDERSTOOD_CONFIG_FIELDS
if nonunderstood:
logger.warning(
"The following configuration fields are not understood: %s",
nonunderstood,
)

self.http_pool = HTTPConnectionPool(reactor=sygnal.reactor)
self.max_connections = self.get_config(
"max_connections", DEFAULT_MAX_CONNECTIONS
)
self.connection_semaphore = DeferredSemaphore(self.max_connections)
self.http_pool.maxPersistentPerHost = self.max_connections

tls_client_options_factory = ClientTLSOptionsFactory()

# use the Sygnal global proxy configuration
proxy_url = sygnal.config.get("proxy")

self.http_agent = ProxyAgent(
reactor=sygnal.reactor,
pool=self.http_pool,
contextFactory=tls_client_options_factory,
proxy_url_str=proxy_url,
)
self.http_agent_wrapper = HttpAgentWrapper(self.http_agent)

privkey_filename = self.get_config("vapid_private_key")
self.vapid_private_key = Vapid.from_file(private_key_file=self.get_config("vapid_private_key"))
vapid_contact_email = self.get_config("vapid_contact_email")
self.vapid_claims = {"sub": "mailto:{}".format(vapid_contact_email)}
bwindels marked this conversation as resolved.
Show resolved Hide resolved

async def _dispatch_notification_unlimited(self, n, device, context):
p256dh = device.pushkey
endpoint = device.data["endpoint"]
session_id = device.data["session_id"]
auth = device.data["auth"]
subscription_info = {
'endpoint': endpoint,
'keys': {
'p256dh': p256dh,
'auth': auth
}
}
payload = {
'room_name': n.room_name,
'room_alias': n.room_alias,
'prio': n.prio,
'membership': n.membership,
'sender_display_name': n.sender_display_name,
'event_id': n.event_id,
'room_id': n.room_id,
'user_is_target': n.user_is_target,
'type': n.type,
'sender': n.sender,
'session_id': session_id,
}
bwindels marked this conversation as resolved.
Show resolved Hide resolved
data = json.dumps(payload)
try:
response_wrapper = webpush(
subscription_info=subscription_info,
data=data,
vapid_private_key=self.vapid_private_key,
vapid_claims=self.vapid_claims,
requests_session=self.http_agent_wrapper
)
response = await response_wrapper.deferred
response_text = (await readBody(response)).decode()
logger.info("webpush provider responded with status: %d, body: %s", response.code, response_text)
except Exception as exception:
raise TemporaryNotificationDispatchException(
"webpush request failure"
) from exception

return []

class HttpAgentWrapper:
clokep marked this conversation as resolved.
Show resolved Hide resolved
def __init__(self, http_agent):
self.http_agent = http_agent

def post(self, endpoint, data, headers, timeout):
clokep marked this conversation as resolved.
Show resolved Hide resolved
logger.info("HttpAgentWrapper: POST %s", endpoint)
body_producer = FileBodyProducer(BytesIO(data))
headers = {
b"User-Agent": ["sygnal"],
b"Content-Encoding": [headers["content-encoding"]],
b"Authorization": [headers["authorization"]],
b"TTL": [headers["ttl"]],
}
deferred = self.http_agent.request(
b"POST",
endpoint.encode(),
headers=Headers(headers),
bodyProducer=body_producer,
)
return HttpResponseWrapper(deferred)

class HttpResponseWrapper:
clokep marked this conversation as resolved.
Show resolved Hide resolved
def __init__(self, deferred):
self.deferred = deferred
self.status_code = 0
self.text = None