This repository has been archived by the owner on Jul 13, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 30
feat: Use FCM HTTPv1 protocol with twisted async #1308
Merged
Merged
Changes from 3 commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,3 +2,4 @@ | |
|
||
# Number of concurrent threads / AWS Session resources | ||
THREAD_POOL_SIZE = 50 | ||
DEFAULT_ROUTER_TIMEOUT = 3 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
"""FCM Router""" | ||
"""FCM legacy HTTP Router""" | ||
from typing import Any # noqa | ||
|
||
import pyfcm | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,165 @@ | ||
"""FCM v1 HTTP Router""" | ||
from typing import Any # noqa | ||
|
||
from twisted.internet.error import ConnectError, TimeoutError | ||
from twisted.logger import Logger | ||
|
||
from autopush.exceptions import RouterException | ||
from autopush.metrics import make_tags | ||
from autopush.router.interface import RouterResponse | ||
from autopush.router.fcm import FCMRouter | ||
from autopush.router.fcmv1client import (FCMv1, FCMAuthenticationError) | ||
from autopush.types import JSONDict # noqa | ||
|
||
|
||
class FCMv1Router(FCMRouter): | ||
"""FCM v1 HTTP Router Implementation | ||
|
||
Note: FCM v1 is a newer version of the FCM HTTP API. | ||
""" | ||
|
||
def __init__(self, conf, router_conf, metrics): | ||
"""Create a new FCM router and connect to FCM""" | ||
self.conf = conf | ||
self.router_conf = router_conf | ||
self.metrics = metrics | ||
self.min_ttl = router_conf.get("ttl", 60) | ||
self.dryRun = router_conf.get("dryrun", False) | ||
self.collapseKey = router_conf.get("collapseKey", "webpush") | ||
self.senderID = router_conf.get("senderID") | ||
self.version = router_conf["version"] | ||
self.log = Logger() | ||
self.fcm = FCMv1(project_id=self.senderID, | ||
service_cred_path=router_conf['service_cred_path'], | ||
logger=self.log, | ||
metrics=self.metrics) | ||
self._base_tags = ["platform:fcmv1"] | ||
self.log.debug("Starting FCMv1 router...") | ||
|
||
def amend_endpoint_response(self, response, router_data): | ||
# type: (JSONDict, JSONDict) -> None | ||
response["senderid"] = self.senderID | ||
|
||
def register(self, uaid, router_data, app_id, *args, **kwargs): | ||
# type: (str, JSONDict, str, *Any, **Any) -> None | ||
"""Validate that the FCM Instance Token is in the ``router_data``""" | ||
senderid = app_id | ||
# "token" is the FCM token generated by the client. | ||
if "token" not in router_data: | ||
raise self._error("connect info missing FCM Instance 'token'", | ||
status=401, | ||
uri=kwargs.get('uri'), | ||
senderid=repr(senderid)) | ||
if senderid != self.senderID: | ||
raise self._error("Invalid SenderID", status=410, errno=105) | ||
# Assign a senderid | ||
router_data["creds"] = {"senderID": self.senderID} | ||
|
||
def route_notification(self, notification, uaid_data): | ||
"""Start the FCM notification routing, returns a deferred""" | ||
router_data = uaid_data["router_data"] | ||
# Kick the entire notification routing off to a thread | ||
return self._route(notification, router_data) | ||
|
||
def _route(self, notification, router_data): | ||
"""Blocking FCM call to route the notification""" | ||
# THIS MUST MATCH THE CHANNELID GENERATED BY THE REGISTRATION SERVICE | ||
# Currently this value is in hex form. | ||
data = {"chid": notification.channel_id.hex} | ||
if not router_data.get("token"): | ||
raise self._error("No registration token found. " | ||
"Rejecting message.", | ||
410, errno=106, log_exception=False) | ||
# Payload data is optional. The endpoint handler validates that the | ||
# correct encryption headers are included with the data. | ||
if notification.data: | ||
mdata = self.router_conf.get('max_data', 4096) | ||
if notification.data_length > mdata: | ||
raise self._error("This message is intended for a " + | ||
"constrained device and is limited " + | ||
"to 3070 bytes. Converted buffer too " + | ||
"long by %d bytes" % | ||
(notification.data_length - mdata), | ||
413, errno=104, log_exception=False) | ||
|
||
data['body'] = notification.data | ||
data['con'] = notification.headers['encoding'] | ||
|
||
if 'encryption' in notification.headers: | ||
data['enc'] = notification.headers['encryption'] | ||
if 'crypto_key' in notification.headers: | ||
data['cryptokey'] = notification.headers['crypto_key'] | ||
elif 'encryption_key' in notification.headers: | ||
data['enckey'] = notification.headers['encryption_key'] | ||
|
||
# registration_ids are the FCM instance tokens (specified during | ||
# registration. | ||
router_ttl = min(self.MAX_TTL, | ||
max(self.min_ttl, notification.ttl or 0)) | ||
d = self.fcm.send( | ||
token=router_data.get("token"), | ||
payload={ | ||
"collapse_key": self.collapseKey, | ||
"data_message": data, | ||
"dry_run": self.dryRun or ('dryrun' in router_data), | ||
"ttl": router_ttl | ||
}) | ||
d.addCallback( | ||
self._process_reply, notification, router_data, router_ttl | ||
) | ||
d.addErrback( | ||
self._process_error | ||
) | ||
return d | ||
|
||
def _process_error(self, failure): | ||
err = failure.value | ||
if isinstance(err, FCMAuthenticationError): | ||
self.log.error("FCM Authentication Error: {}".format(err)) | ||
raise RouterException("Server error", status_code=500, errno=901) | ||
if isinstance(err, TimeoutError): | ||
self.log.warn("FCM Timeout: %s" % err) | ||
self.metrics.increment("notification.bridge.error", | ||
tags=make_tags( | ||
self._base_tags, | ||
reason="timeout")) | ||
raise RouterException("Server error", status_code=502, | ||
errno=903, | ||
log_exception=False) | ||
if isinstance(err, ConnectError): | ||
self.log.warn("FCM Unavailable: %s" % err) | ||
self.metrics.increment("notification.bridge.error", | ||
tags=make_tags( | ||
self._base_tags, | ||
reason="connection_unavailable")) | ||
raise RouterException("Server error", status_code=502, | ||
errno=902, | ||
log_exception=False) | ||
if isinstance(err, RouterException): | ||
self.log.warn("FCM Error: {}".format(err)) | ||
self.metrics.increment("notification.bridge.error", | ||
tags=make_tags( | ||
self._base_tags, | ||
reason="server_error")) | ||
return failure | ||
|
||
def _error(self, err, status, **kwargs): | ||
"""Error handler that raises the RouterException""" | ||
self.log.debug(err, **kwargs) | ||
return RouterException(err, status_code=status, response_body=err, | ||
**kwargs) | ||
|
||
def _process_reply(self, reply, notification, router_data, ttl): | ||
"""Process FCM send reply""" | ||
# Failures are returned as non-200 messages (404, 410, etc.) | ||
self.metrics.increment("notification.bridge.sent", | ||
tags=self._base_tags) | ||
self.metrics.increment("notification.message_data", | ||
notification.data_length, | ||
tags=make_tags(self._base_tags, | ||
destination="Direct")) | ||
location = "%s/m/%s" % (self.conf.endpoint_url, notification.version) | ||
return RouterResponse(status_code=201, response_body="", | ||
headers={"TTL": ttl, | ||
"Location": location}, | ||
logged_status=200) |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thread?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
bad comment, thanks!