Skip to content
This repository has been archived by the owner on Jul 13, 2023. It is now read-only.

Commit

Permalink
Merge pull request #1082 from mozilla-services/release/1.39.1-integra…
Browse files Browse the repository at this point in the history
…tion

1.39.1 candidate -> release/1.39
  • Loading branch information
pjenvey authored Nov 20, 2017
2 parents 4ef57a2 + 443c368 commit 3412b9a
Show file tree
Hide file tree
Showing 13 changed files with 565 additions and 100 deletions.
17 changes: 16 additions & 1 deletion autopush/router/apnsrouter.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""APNS Router"""
import ssl
import uuid
from typing import Any # noqa

Expand Down Expand Up @@ -137,7 +138,10 @@ def _route(self, notification, router_data):
payload["enckey"] = notification.headers["encryption_key"]
payload['aps'] = router_data.get('aps', {
"mutable-content": 1,
"alert": {"title": " ", "body": " "}
"alert": {
"loc-key": "SentTab.NoTabArrivingNotification.body",
"title-loc-key": "SentTab.NoTabArrivingNotification.title",
}
})
apns_id = str(uuid.uuid4()).lower()
# APNs may force close a connection on us without warning.
Expand All @@ -158,6 +162,17 @@ def _route(self, notification, router_data):
tags=make_tags(self._base_tags,
application=rel_channel,
reason="http2_error"))
except ssl.SSLError as e:
# can only str match this (for autopush#1048)
if not (e.errno == ssl.SSL_ERROR_SSL and
str(e).startswith("[SSL: BAD_WRITE_RETRY]")):
raise # pragma: nocover
self.metrics.increment(
"notification.bridge.connection.error",
tags=make_tags(self._base_tags,
application=rel_channel,
reason="bad_write_retry_error")
)
if not success:
raise RouterException(
"Server error",
Expand Down
26 changes: 16 additions & 10 deletions autopush/router/gcm.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
"""GCM Router"""
from typing import Any # noqa

import gcmclient
from requests.exceptions import ConnectionError
from twisted.internet.threads import deferToThread
from twisted.logger import Logger

from autopush.exceptions import RouterException
from autopush.metrics import make_tags
from autopush.router import gcmclient
from autopush.router.interface import RouterResponse
from autopush.types import JSONDict # noqa

Expand Down Expand Up @@ -35,10 +35,7 @@ def __init__(self, conf, router_conf, metrics):
for sid in router_conf.get("senderIDs"):
auth = router_conf.get("senderIDs").get(sid).get("auth")
self.senderIDs[sid] = auth
try:
self.gcm[sid] = gcmclient.GCM(auth)
except Exception:
raise IOError("GCM Bridge not initiated in main")
self.gcm[sid] = gcmclient.GCM(auth)
self._base_tags = ["platform:gcm"]
self.log.debug("Starting GCM router...")

Expand Down Expand Up @@ -115,19 +112,24 @@ def _route(self, notification, uaid_data):
try:
gcm = self.gcm[router_data['creds']['senderID']]
result = gcm.send(payload)
except RouterException:
raise # pragma nocover
except KeyError:
self.log.critical("Missing GCM bridge credentials")
raise RouterException("Server error", status_code=500)
raise RouterException("Server error", status_code=500,
errno=900)
except gcmclient.GCMAuthenticationError as e:
self.log.error("GCM Authentication Error: %s" % e)
raise RouterException("Server error", status_code=500)
raise RouterException("Server error", status_code=500,
errno=901)
except ConnectionError as e:
self.log.warn("GCM Unavailable: %s" % e)
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)
except Exception as e:
self.log.error("Unhandled exception in GCM Routing: %s" % e)
Expand All @@ -146,7 +148,7 @@ def _process_reply(self, reply, uaid_data, ttl, notification):
# acks:
# for reg_id, msg_id in reply.success.items():
# updates
for old_id, new_id in reply.canonical.items():
for old_id, new_id in reply.canonicals.items():
self.log.debug("GCM id changed : {old} => {new}",
old=old_id, new=new_id)
self.metrics.increment("notification.bridge.error",
Expand Down Expand Up @@ -181,15 +183,19 @@ def _process_reply(self, reply, uaid_data, ttl, notification):
)

# retries:
if reply.needs_retry():
if reply.retry_after:
self.metrics.increment("notification.bridge.error",
tags=make_tags(self._base_tags,
reason="retry"))
self.log.warn("GCM retry requested: {failed()}",
failed=lambda: repr(reply.failed.items()))
raise RouterException("GCM failure to deliver, retry",
status_code=503,
response_body="Please try request later.",
headers={"Retry-After": reply.retry_after},
response_body="Please try request "
"in {} seconds.".format(
reply.retry_after
),
log_exception=False)

self.metrics.increment("notification.bridge.sent",
Expand Down
157 changes: 157 additions & 0 deletions autopush/router/gcmclient.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import json

import requests

from autopush.exceptions import RouterException


class GCMAuthenticationError(Exception):
pass


class Result(object):
"""Abstraction object for GCM response"""

def __init__(self, message, response):
"""Process GCM message and response into abstracted object
:param message: Message payload
:type message: JSONMessage
:param response: GCM response
:type response: requests.Response
"""
self.success = {}
self.canonicals = {}
self.unavailable = []
self.not_registered = []
self.failed = {}

self.message = message
self.retry_message = None

self.retry_after = response.headers.get('Retry-After', None)

if response.status_code != 200:
self.retry_message = message
else:
self._parse_response(message, response.content)

def _parse_response(self, message, content):
data = json.loads(content)
if not data.get('results'):
raise RouterException("Recv'd invalid response from GCM")
reg_id = message.payload['registration_ids'][0]
for res in data['results']:
if 'message_id' in res:
self.success[reg_id] = res['message_id']
if 'registration_id' in res:
self.canonicals[reg_id] = res['registration_id']
else:
if res['error'] in ['Unavailable', 'InternalServerError']:
self.unavailable.append(reg_id)
elif res['error'] == 'NotRegistered':
self.not_registered.append(reg_id)
else:
self.failed[reg_id] = res['error']


class JSONMessage(object):
"""GCM formatted payload
"""
def __init__(self,
registration_ids,
collapse_key,
time_to_live,
dry_run,
data):
"""Convert data elements into a GCM payload.
:param registration_ids: Single or list of registration ids to send to
:type registration_ids: str or list
:param collapse_key: GCM collapse key for the data.
:type collapse_key: str
:param time_to_live: Seconds to keep message alive
:type time_to_live: int
:param dry_run: GCM Dry run flag to allow remote verification
:type dry_run: bool
:param data: Data elements to send
:type data: dict
"""
if not registration_ids:
raise RouterException("No Registration IDs specified")
if not isinstance(registration_ids, list):
registration_ids = [registration_ids]
self.registration_ids = registration_ids
self.payload = {
'registration_ids': self.registration_ids,
'collapse_key': collapse_key,
'time_to_live': int(time_to_live),
'delay_while_idle': False,
'dry_run': bool(dry_run),
}
if data:
self.payload['data'] = data


class GCM(object):
"""Primitive HTTP GCM service handler."""

def __init__(self,
api_key=None,
logger=None,
metrics=None,
endpoint="gcm-http.googleapis.com/gcm/send",
**options):

"""Initialize the GCM primitive.
:param api_key: The GCM API key (from the Google developer console)
:type api_key: str
:param logger: Status logger
:type logger: logger
:param metrics: Metric recorder
:type metrics: autopush.metrics.IMetric
:param endpoint: GCM endpoint override
:type endpoint: str
:param options: Additional options
:type options: dict
"""
self._endpoint = "https://{}".format(endpoint)
self._api_key = api_key
self.metrics = metrics
self.log = logger
self._options = options
self._sender = requests.post

def send(self, payload):
"""Send a payload to GCM
:param payload: Dictionary of GCM formatted data
:type payload: JSONMessage
:return: Result
"""
headers = {
'Content-Type': 'application/json',
'Authorization': 'key={}'.format(self._api_key),
}

response = self._sender(
url=self._endpoint,
headers=headers,
data=json.dumps(payload.payload),
**self._options
)

if response.status_code in (400, 404):
raise RouterException(response.content)

if response.status_code == 401:
raise GCMAuthenticationError("Authentication Error")

if response.status_code == 200 or (500 <= response.status_code <= 599):
return Result(payload, response)
8 changes: 7 additions & 1 deletion autopush/ssl.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@
from OpenSSL import SSL
from twisted.internet.ssl import DefaultOpenSSLContextFactory

try:
SSL_PROTO = ssl.PROTOCOL_TLS
except AttributeError:
SSL_PROTO = ssl.PROTOCOL_SSLv23


MOZILLA_INTERMEDIATE_CIPHERS = (
'ECDHE-RSA-AES128-GCM-SHA256:'
'ECDHE-ECDSA-AES128-GCM-SHA256:'
Expand Down Expand Up @@ -111,7 +117,7 @@ def ssl_wrap_socket_cached(
certfile=None, # type: Optional[str]
server_side=False, # type: bool
cert_reqs=ssl.CERT_NONE, # type: int
ssl_version=ssl.PROTOCOL_TLS, # type: int
ssl_version=SSL_PROTO, # type: int
ca_certs=None, # type: Optional[str]
do_handshake_on_connect=True, # type: bool
suppress_ragged_eofs=True, # type: bool
Expand Down
Loading

0 comments on commit 3412b9a

Please sign in to comment.