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

Commit

Permalink
feat: allow multi app-ids and auths for fcm
Browse files Browse the repository at this point in the history
BREAKING CHANGE:
This now supports multiple registrations for FCM. See the updated docs
and sample configurations.

Obsolete configuration options:
    * fcm_auth
    * fcm_senderid
    * fcm_projectid
    * fcm_service_cred_path

New Configuration
```json
fcm_creds={"_profile_": {"projectid": "_fcm_projectid_", "auth":
"_fcm_service_cred_path_"}}
```

`v1` protocol is now the default.

for existing FCM, _profile_ == _fcm_projectid_

Closes #1340
  • Loading branch information
jrconlin committed Aug 5, 2019
1 parent e404674 commit 54b3351
Show file tree
Hide file tree
Showing 10 changed files with 238 additions and 178 deletions.
43 changes: 22 additions & 21 deletions autopush/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -283,28 +283,29 @@ def from_argparse(cls, ns, **kwargs):
"Invalid client_certs argument")
client_certs[sig] = name

if ns.fcm_enabled:
fcm_core = {
"ttl": ns.fcm_ttl,
"dryrun": ns.fcm_dryrun,
"max_data": ns.max_data,
"collapsekey": ns.fcm_collapsekey}
if len(ns.fcm_auth) > 0:
if not ns.fcm_senderid:
raise InvalidConfig("No SenderID found for FCM")
fcm_core.update({
"version": 0,
"auth": ns.fcm_auth,
"senderID": ns.fcm_senderid})
if len(ns.fcm_service_cred_path) > 0:
fcm_core.update({
if ns.fcm_enabled and ns.fcm_creds:
try:
router_conf["fcm"] = {
"version": ns.fcm_version,
"ttl": ns.fcm_ttl,
"version": 1,
"service_cred_path": ns.fcm_service_cred_path,
"senderID": ns.fcm_project_id})
if "version" not in fcm_core:
raise InvalidConfig("No credential info found for FCM")
router_conf["fcm"] = fcm_core
"dryrun": ns.fcm_dryrun,
"max_data": ns.max_data,
"collapsekey": ns.fcm_collapsekey,
"creds": json.loads(ns.fcm_creds)
}
if not router_conf["fcm"]["creds"]:
raise InvalidConfig(
"Empty credentials for FCM config options"
)
for creds in router_conf["fcm"]["creds"].values():
if "auth" not in creds:
raise InvalidConfig(
"Missing auth for FCM config options"
)
except (ValueError, TypeError):
raise InvalidConfig(
"Invalid JSON specified for FCM config options"
)

if ns.adm_creds:
# Create a common admclient
Expand Down
28 changes: 13 additions & 15 deletions autopush/main_argparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,11 @@ def _obsolete_args(parser):
parser.add_argument('--storage_read_throughput', help="OBSOLETE")
parser.add_argument('--storage_write_throughput', help="OBSOLETE")

parser.add_argument('--fcm_enabled', help="OBSOLETE")
parser.add_argument('--fcm_project_id', help="OBSOLETE")
parser.add_argument('--fcm_service_cred_path', help="OBSOLETE")
parser.add_argument('--fcm_senderid_list', help="OBSOLETE")


def _add_external_router_args(parser):
"""Parses out external router arguments"""
Expand All @@ -165,10 +170,10 @@ def _add_external_router_args(parser):
type=str, default="gcm-http.googleapis.com/gcm/send",
env_var="GCM_ENDPOINT")
# FCM
parser.add_argument('--fcm_enabled', help="Enable FCM Bridge",
action="store_true", default=False,
env_var="FCM_ENABLED")
label = "FCM Router:"
label = "FCM Router"
parser.add_argument('--fcm_creds',
help="JSON dictionary of {} settings".format(label),
type=str, default="", env_var="FCM_CREDS")
parser.add_argument('--fcm_ttl', help="%s Time to Live" % label,
type=int, default=60, env_var="FCM_TTL")
parser.add_argument('--fcm_dryrun',
Expand All @@ -179,17 +184,10 @@ def _add_external_router_args(parser):
help="%s string to collapse messages" % label,
type=str, default="simplepush",
env_var="FCM_COLLAPSEKEY")
parser.add_argument('--fcm_auth', help='Auth Key for FCM',
type=str, default="", env_var="FCM_AUTH")
parser.add_argument('--fcm_senderid', help='SenderID for FCM',
type=str, default="", env_var="FCM_SENDERID")
# FCM v1 HTTP API
parser.add_argument('--fcm_project_id', help="FCM Project identifier",
type=str, default="", env_var="FCM_PROJECT_ID")
parser.add_argument('--fcm_service_cred_path',
help="Path to FCM Service Credentials",
type=str, default="",
env_var="FCM_SERVICE_CRED_PATH")
# Specify which FCM version you're using here.
parser.add_argument('--fcm_version',
help="{} version (0=legacy, 1=v1)".format(label),
type=int, default=1, env_var="FCM_VERSION")

# Apple Push Notification system (APNs) for iOS
# credentials consist of JSON struct containing a channel type
Expand Down
46 changes: 27 additions & 19 deletions autopush/router/fcm.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,14 @@ class FCMRouter(object):
}
}

def _setter(self, c):
(sid, creds) = c
try:
self.clients[sid] = pyfcm.FCMNotification(api_key=creds["auth"])
except Exception as e:
self.log.error("Could not instantiate FCM {ex}", ex=e)
raise IOError("FCM Bridge not initiated in main")

def __init__(self, conf, router_conf, metrics):
"""Create a new FCM router and connect to FCM"""
self.conf = conf
Expand All @@ -109,42 +117,37 @@ def __init__(self, conf, router_conf, 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.auth = router_conf.get("auth")
self._base_tags = ["platform:fcm"]
self.clients = {}
try:
self.fcm = pyfcm.FCMNotification(api_key=self.auth)
except Exception as e:
self.log.error("Could not instantiate FCM {ex}",
ex=e)
map(self._setter, router_conf["creds"].items())
except KeyError:
self.log.error("Could not instantiate FCM: missing credentials")
raise IOError("FCM Bridge not initiated in main")
self._base_tags = ["platform:fcm"]
self.log.debug("Starting FCM router...")

def amend_endpoint_response(self, response, router_data):
# type: (JSONDict, JSONDict) -> None
response["senderid"] = router_data.get('creds', {}).get('senderID')
response["senderid"] = router_data.get('app_id')

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 GCM registration id token generated by the client.
# "token" is the FCM registration id 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))
senderid=repr(app_id))
# senderid is the remote client's senderID value. This value is
# very difficult for the client to change, and there was a problem
# where some clients had an older, invalid senderID. We need to
# be able to match senderID to it's corresponding auth key.
# If the client has an unexpected or invalid SenderID,
# it is impossible for us to reach them.
if not (senderid == self.senderID):
if app_id not in self.clients:
raise self._error("Invalid SenderID", status=410, errno=105)
# Assign a senderid
router_data["creds"] = {"senderID": self.senderID,
"auth": self.auth}
router_data["app_id"] = app_id

def route_notification(self, notification, uaid_data):
"""Start the FCM notification routing, returns a deferred"""
Expand Down Expand Up @@ -183,13 +186,19 @@ def _route(self, notification, router_data):
data['cryptokey'] = notification.headers['crypto_key']
elif 'encryption_key' in notification.headers:
data['enckey'] = notification.headers['encryption_key']
try:
client = self.clients[router_data["app_id"]]
except KeyError:
self.log.critical("Missing FCM bridge credentials for {id}",
id=router_data['app_id'])
raise RouterException("Server error", status_code=500, error=901)

# registration_ids are the FCM instance tokens (specified during
# registration.
router_ttl = min(self.MAX_TTL,
max(self.min_ttl, notification.ttl or 0))
try:
result = self.fcm.notify_single_device(
result = client.notify_single_device(
collapse_key=self.collapseKey,
data_message=data,
dry_run=self.dryRun or ('dryrun' in router_data),
Expand Down Expand Up @@ -247,18 +256,17 @@ def _process_reply(self, reply, notification, router_data, ttl):
err['msg'],
nlen=notification.data_length,
regid=router_data["token"],
senderid=self.senderID,
senderid=router_data.get('token'),
ttl=notification.ttl,
)
raise RouterException("FCM failure to deliver",
status_code=err['err'],
response_body="Please try request "
"later.",
log_exception=False)
creds = router_data["creds"]
self.log.debug("{msg} : {info}",
msg=err['msg'],
info={"senderid": creds.get('registration_id'),
info={"app_id": router_data["app_id"],
"reason": reason})
return RouterResponse(
status_code=err['err'],
Expand Down
53 changes: 34 additions & 19 deletions autopush/router/fcm_v1.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,18 @@ class FCMv1Router(FCMRouter):
Note: FCM v1 is a newer version of the FCM HTTP API.
"""

def _setter(self, c):
(sid, creds) = c
try:
self.clients[sid] = FCMv1(
project_id=creds["projectid"],
service_cred_path=creds["auth"],
logger=self.log,
metrics=self.metrics)
except Exception as e:
self.log.error("Could not instantiate FCMv1 {ex}", ex=e)
raise IOError("FCMv1 Bridge not initiated in main")

def __init__(self, conf, router_conf, metrics):
"""Create a new FCM router and connect to FCM"""
self.conf = conf
Expand All @@ -26,34 +38,33 @@ def __init__(self, conf, router_conf, 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.clients = {}
try:
map(self._setter, router_conf["creds"].items())
except KeyError:
self.log.error("Could not instantiate FCMv1: missing credentials")
raise IOError("FCMv1 Bridge not initiated in main")
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
response["senderid"] = router_data["app_id"]

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:
senderid=repr(app_id))
if app_id not in self.clients:
raise self._error("Invalid SenderID", status=410, errno=105)
# Assign a senderid
router_data["creds"] = {"senderID": self.senderID}
router_data["app_id"] = app_id

def route_notification(self, notification, uaid_data):
"""Start the FCM notification routing, returns a deferred"""
Expand Down Expand Up @@ -95,14 +106,18 @@ def _route(self, notification, router_data):
# 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
})
try:
d = self.clients[router_data["app_id"]].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
})
except KeyError:
raise self._error("Invalid Application ID specified",
404, errno=106, log_exception=False)
d.addCallback(
self._process_reply, notification, router_data, router_ttl
)
Expand Down
12 changes: 7 additions & 5 deletions autopush/tests/test_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -1907,12 +1907,13 @@ def _add_router(self):
"dryrun": True,
"max_data": 4096,
"collapsekey": "test",
"senderID": self.senderID,
"auth": "AIzaSyCx9PRtH8ByaJR3CfJamz0D2N0uaCgRGiI",
"creds": {
self.senderID: {
"auth": "AIzaSyCx9PRtH8ByaJR3CfJamz0D2N0uaCgRGiI"}
},
},
self.ep.db.metrics
)
self.ep.routers["fcm"] = fcm
# Set up the mock call to avoid calling the live system.
# The problem with calling the live system (even sandboxed) is that
# you need a valid credential set from a mobile device, which can be
Expand All @@ -1923,8 +1924,9 @@ def _add_router(self):
results=[{}],
)
self._mock_send = Mock()
fcm.fcm.send_request = self._mock_send
fcm.fcm.parse_responses = Mock(return_value=reply)
fcm.clients[self.senderID].send_request = self._mock_send
fcm.clients[self.senderID].parse_responses = Mock(return_value=reply)
self.ep.routers["fcm"] = fcm

@inlineCallbacks
def test_registration(self):
Expand Down
Loading

0 comments on commit 54b3351

Please sign in to comment.