def make_settings(args, **kwargs): """Helper function to make a :class:`AutopushSettings` object""" router_conf = {} if args.key_hash: db.key_hash = args.key_hash # Some routers require a websocket to timeout on idle (e.g. UDP) if args.wake_pem is not None and args.wake_timeout != 0: router_conf["simplepush"] = {"idle": args.wake_timeout, "server": args.wake_server, "cert": args.wake_pem} if args.apns_enabled: # if you have the critical elements for each external router, create it if args.apns_cert_file is not None and args.apns_key_file is not None: router_conf["apns"] = {"sandbox": args.apns_sandbox, "cert_file": args.apns_cert_file, "key_file": args.apns_key_file} if args.gcm_enabled: # Create a common gcmclient slist = json.loads(args.senderid_list) senderIDs = SenderIDs(dict( s3_bucket=args.s3_bucket, senderid_expry=args.senderid_expry, use_s3=args.s3_bucket.lower() != "none", senderid_list=slist)) # This is an init check to verify that things are configured # correctly. Otherwise errors may creep in later that go # unaccounted. senderID = senderIDs.choose_ID() if senderID is None: log.err("No GCM SenderIDs specified or found.") return router_conf["gcm"] = {"ttl": args.gcm_ttl, "dryrun": args.gcm_dryrun, "max_data": args.max_data, "collapsekey": args.gcm_collapsekey, "senderIDs": senderIDs, "senderid_list": list} return AutopushSettings( crypto_key=args.crypto_key, datadog_api_key=args.datadog_api_key, datadog_app_key=args.datadog_app_key, datadog_flush_interval=args.datadog_flush_interval, hostname=args.hostname, statsd_host=args.statsd_host, statsd_port=args.statsd_port, router_conf=router_conf, router_tablename=args.router_tablename, storage_tablename=args.storage_tablename, storage_read_throughput=args.storage_read_throughput, storage_write_throughput=args.storage_write_throughput, message_tablename=args.message_tablename, message_read_throughput=args.message_read_throughput, message_write_throughput=args.message_write_throughput, router_read_throughput=args.router_read_throughput, router_write_throughput=args.router_write_throughput, resolve_hostname=args.resolve_hostname, wake_timeout=args.wake_timeout, **kwargs )
class GCMRouter(object): """GCM Router Implementation""" gcm = None ttl = 60 dryRun = 0 collapseKey = "simplepush" creds = {} def __init__(self, ap_settings, router_conf): """Create a new GCM router and connect to GCM""" self.config = router_conf self.ttl = router_conf.get("ttl", 60) self.dryRun = router_conf.get("dryrun", False) self.collapseKey = router_conf.get("collapseKey", "simplepush") self.senderIDs = router_conf.get("senderIDs") if not self.senderIDs: self.senderIDs = SenderIDs(router_conf) try: senderID = self.senderIDs.choose_ID() self.gcm = gcmclient.GCM(senderID.get("auth")) except: raise IOError("GCM Bridge not initiated in main") log.msg("Starting GCM router...") def amend_msg(self, msg): msg["senderid"] = self.creds.get("senderID") return msg def register(self, uaid, router_data): """Validate that a token is in the ``router_data``""" if not router_data.get("token"): self._error("connect info missing 'token'", status=401) # Assign a senderid router_data["creds"] = self.creds = self.senderIDs.choose_ID() return router_data def route_notification(self, notification, uaid_data): """Start the GCM notification routing, returns a deferred""" router_data = uaid_data["router_data"] # Kick the entire notification routing off to a thread return deferToThread(self._route, notification, router_data) def _route(self, notification, router_data): """Blocking GCM call to route the notification""" data={"chid": notification.channel_id, "ver": notification.version} # Payload data is optional. If present, all of Content-Encoding, # Encryption, and Encryption-Key are required. If one or more are # missing, a 400 response is produced. con = notification.headers.get('content-encoding', None) enc = notification.headers.get('encryption', None) enckey = notification.headers.get('encryption-key', None) if notification.data: if not con: self._error("notification with data is missing header: Content-Encoding", 400) if not enc: self._error("notification with data is missing header: Encryption", 400) if not enckey: self._error("notification with data is missing header: Encryption-Key", 400) data['body'] = notification.data data['con'] = con data['enc'] = enc data['enckey'] = enckey payload = gcmclient.JSONMessage( registration_ids=[router_data["token"]], collapse_key=self.collapseKey, time_to_live=self.ttl, dry_run=self.dryRun, data=data, ) creds = router_data.get("creds", {"senderID": "missing id"}) try: self.gcm.api_key = creds["auth"] result = self.gcm.send(payload) except KeyError: self._error("Server error, missing bridge credentials for %s" % creds.get("senderID"), 500) except gcmclient.GCMAuthenticationError, e: self._error("Authentication Error: %s" % e, 500) except Exception, e: self._error("Unhandled exception in GCM Routing: %s" % e, 500)
class GCMRouter(object): """GCM Router Implementation""" log = Logger() gcm = None dryRun = 0 collapseKey = "simplepush" def __init__(self, ap_settings, router_conf): """Create a new GCM router and connect to GCM""" self.config = router_conf self.min_ttl = router_conf.get("ttl", 60) self.dryRun = router_conf.get("dryrun", False) self.collapseKey = router_conf.get("collapseKey", "simplepush") self.senderIDs = router_conf.get("senderIDs") if not self.senderIDs: self.senderIDs = SenderIDs(router_conf) try: senderID = self.senderIDs.choose_ID() self.gcm = gcmclient.GCM(senderID.get("auth")) except: raise IOError("GCM Bridge not initiated in main") self.log.debug("Starting GCM router...") def check_token(self, token): if token not in self.senderIDs.senderIDs(): return (False, self.senderIDs.choose_ID().get('senderID')) return (True, token) def amend_msg(self, msg, data=None): if data is not None: msg["senderid"] = data.get('creds', {}).get('senderID') return msg def register(self, uaid, router_data, router_token=None, *kwargs): """Validate that the GCM Instance Token is in the ``router_data``""" if "token" not in router_data: raise self._error("connect info missing GCM Instance 'token'", status=401) # Assign a senderid router_data["creds"] = self.senderIDs.get_ID(router_token) return router_data def route_notification(self, notification, uaid_data): """Start the GCM notification routing, returns a deferred""" router_data = uaid_data["router_data"] # Kick the entire notification routing off to a thread return deferToThread(self._route, notification, router_data) def _route(self, notification, router_data): """Blocking GCM call to route the notification""" data = {"chid": notification.channel_id} # Payload data is optional. The endpoint handler validates that the # correct encryption headers are included with the data. if notification.data: mdata = self.config.get('max_data', 4096) if len(notification.data) > 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" % (len(notification.data) - mdata), 413, errno=104) data['body'] = notification.data data['con'] = notification.headers['content-encoding'] 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 GCM instance tokens (specified during # registration. router_ttl = notification.ttl or 0 payload = gcmclient.JSONMessage( registration_ids=[router_data.get("token")], collapse_key=self.collapseKey, time_to_live=max(self.min_ttl, router_ttl), dry_run=self.dryRun or ("dryrun" in router_data), data=data, ) creds = router_data.get("creds", {"senderID": "missing id"}) try: self.gcm.api_key = creds["auth"] result = self.gcm.send(payload) except KeyError: raise self._error("Server error, missing bridge credentials " + "for %s" % creds.get("senderID"), 500) except gcmclient.GCMAuthenticationError, e: raise self._error("Authentication Error: %s" % e, 500) except Exception, e: raise self._error("Unhandled exception in GCM Routing: %s" % e, 500)
class GCMRouter(object): """GCM Router Implementation""" gcm = None ttl = 60 dryRun = 0 collapseKey = "simplepush" creds = {} def __init__(self, ap_settings, router_conf): """Create a new GCM router and connect to GCM""" self.config = router_conf self.ttl = router_conf.get("ttl", 60) self.dryRun = router_conf.get("dryrun", False) self.collapseKey = router_conf.get("collapseKey", "simplepush") self.senderIDs = router_conf.get("senderIDs") if not self.senderIDs: self.senderIDs = SenderIDs(router_conf) try: senderID = self.senderIDs.choose_ID() self.gcm = gcmclient.GCM(senderID.get("auth")) except: raise IOError("GCM Bridge not initiated in main") log.msg("Starting GCM router...") def amend_msg(self, msg): msg["senderid"] = self.creds.get("senderID") return msg def register(self, uaid, router_data): """Validate that a token is in the ``router_data``""" if not router_data.get("token"): self._error("connect info missing 'token'", status=401) # Assign a senderid router_data["creds"] = self.creds = self.senderIDs.choose_ID() return router_data def route_notification(self, notification, uaid_data): """Start the GCM notification routing, returns a deferred""" router_data = uaid_data["router_data"] # Kick the entire notification routing off to a thread return deferToThread(self._route, notification, router_data) def _route(self, notification, router_data): """Blocking GCM call to route the notification""" payload = gcmclient.JSONMessage( registration_ids=[router_data["token"]], collapse_key=self.collapseKey, time_to_live=self.ttl, dry_run=self.dryRun, data={"Msg": notification.data, "Chid": notification.channel_id, "Ver": notification.version} ) creds = router_data.get("creds", {"senderID": "missing id"}) try: self.gcm.api_key = creds["auth"] result = self.gcm.send(payload) except KeyError: self._error("Server error, missing bridge credentials for %s" % creds.get("senderID"), 500) except gcmclient.GCMAuthenticationError, e: self._error("Authentication Error: %s" % e, 500) except Exception, e: self._error("Unhandled exception in GCM Routing: %s" % e, 500)
def make_settings(args, **kwargs): """Helper function to make a :class:`AutopushSettings` object""" router_conf = {} if args.key_hash: db.key_hash = args.key_hash # Some routers require a websocket to timeout on idle (e.g. UDP) if args.wake_pem is not None and args.wake_timeout != 0: router_conf["simplepush"] = { "idle": args.wake_timeout, "server": args.wake_server, "cert": args.wake_pem } if args.apns_enabled: # if you have the critical elements for each external router, create it if args.apns_cert_file is not None and args.apns_key_file is not None: router_conf["apns"] = { "sandbox": args.apns_sandbox, "cert_file": args.apns_cert_file, "key_file": args.apns_key_file } if args.gcm_enabled: # Create a common gcmclient slist = json.loads(args.senderid_list) senderIDs = SenderIDs( dict(s3_bucket=args.s3_bucket, senderid_expry=args.senderid_expry, use_s3=args.s3_bucket.lower() != "none", senderid_list=slist)) # This is an init check to verify that things are configured # correctly. Otherwise errors may creep in later that go # unaccounted. senderID = senderIDs.choose_ID() if senderID is None: log.critical(format="No GCM SenderIDs specified or found.") return router_conf["gcm"] = { "ttl": args.gcm_ttl, "dryrun": args.gcm_dryrun, "max_data": args.max_data, "collapsekey": args.gcm_collapsekey, "senderIDs": senderIDs, "senderid_list": list } return AutopushSettings( crypto_key=args.crypto_key, datadog_api_key=args.datadog_api_key, datadog_app_key=args.datadog_app_key, datadog_flush_interval=args.datadog_flush_interval, hostname=args.hostname, statsd_host=args.statsd_host, statsd_port=args.statsd_port, router_conf=router_conf, router_tablename=args.router_tablename, storage_tablename=args.storage_tablename, storage_read_throughput=args.storage_read_throughput, storage_write_throughput=args.storage_write_throughput, message_tablename=args.message_tablename, message_read_throughput=args.message_read_throughput, message_write_throughput=args.message_write_throughput, router_read_throughput=args.router_read_throughput, router_write_throughput=args.router_write_throughput, resolve_hostname=args.resolve_hostname, wake_timeout=args.wake_timeout, **kwargs)
class GCMRouter(object): """GCM Router Implementation""" gcm = None ttl = 60 dryRun = 0 collapseKey = "simplepush" creds = {} def __init__(self, ap_settings, router_conf): """Create a new GCM router and connect to GCM""" self.config = router_conf self.ttl = router_conf.get("ttl", 60) self.dryRun = router_conf.get("dryrun", False) self.collapseKey = router_conf.get("collapseKey", "simplepush") self.senderIDs = router_conf.get("senderIDs") if not self.senderIDs: self.senderIDs = SenderIDs(router_conf) try: senderID = self.senderIDs.choose_ID() self.gcm = gcmclient.GCM(senderID.get("auth")) except: raise IOError("GCM Bridge not initiated in main") log.msg("Starting GCM router...") def check_token(self, token): if token not in self.senderIDs.senderIDs(): return (False, self.senderIDs.choose_ID().get('senderID')) return (True, token) def amend_msg(self, msg): msg["senderid"] = self.creds.get("senderID") return msg def register(self, uaid, router_data): """Validate that a token is in the ``router_data``""" if not router_data.get("token"): raise self._error("connect info missing 'token'", status=401) # Assign a senderid router_data["creds"] = self.creds = self.senderIDs.choose_ID() return router_data def route_notification(self, notification, uaid_data): """Start the GCM notification routing, returns a deferred""" router_data = uaid_data["router_data"] # Kick the entire notification routing off to a thread return deferToThread(self._route, notification, router_data) def _route(self, notification, router_data): """Blocking GCM call to route the notification""" data = {"chid": notification.channel_id, "ver": notification.version} # Payload data is optional. If present, all of Content-Encoding, # Encryption, and Encryption/Crypto-Key are required. If one or # more are missing, a 400 response is produced. if notification.data: lead = "notification with data is missing header:" con = notification.headers.get('content-encoding', None) if not con: raise self._error("%s Content-Encoding" % lead, 400) enc = notification.headers.get('encryption', None) if not enc: raise self._error("%s Encryption" % lead, 400) if ('crypto-key' in notification.headers and 'encryption-key' in notification.headers): raise self._error("notification with data has both" "crypto-key and encryption-key headers", 400) if not ('crypto-key' in notification.headers or 'encryption-key' in notification.headers): raise self._error("notification with data is missing " + "key header", 400) if ('encryption-key' in notification.headers): data['enckey'] = notification.headers.get('encryption-key') if ('crypto-key' in notification.headers): data['cryptokey'] = notification.headers.get('crypto-key') udata = urlsafe_b64encode(notification.data) mdata = self.config.get('max_data', 4096) if len(udata) > 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" % (len(udata) - mdata), 413, errno=104) # TODO: if the data is longer than max_data, raise error data['body'] = udata data['con'] = con data['enc'] = enc payload = gcmclient.JSONMessage( registration_ids=[router_data["token"]], collapse_key=self.collapseKey, time_to_live=self.ttl, dry_run=self.dryRun, data=udata, ) creds = router_data.get("creds", {"senderID": "missing id"}) try: self.gcm.api_key = creds["auth"] result = self.gcm.send(payload) except KeyError: raise self._error("Server error, missing bridge credentials " + "for %s" % creds.get("senderID"), 500) except gcmclient.GCMAuthenticationError, e: raise self._error("Authentication Error: %s" % e, 500) except Exception, e: raise self._error("Unhandled exception in GCM Routing: %s" % e, 500)
class SenderIDsTestCase(unittest.TestCase): def setUp(self): mock_dynamodb2().start() mock_s3().start() self.senderIDs = None def tearDown(self): mock_dynamodb2().stop() mock_s3().stop() if self.senderIDs: self.senderIDs.stop() def test_nos3(self): self.senderIDs = SenderIDs(dict(use_s3=False)) self.senderIDs.conn = Mock() self.senderIDs._refresh() eq_(self.senderIDs.conn.get_bucket.call_count, 0) def test_bad_init(self): self.senderIDs = SenderIDs(dict(senderid_list="[Update")) eq_(self.senderIDs._senderIDs, {}) def test_success(self): settings = dict( s3_bucket=TEST_BUCKET, senderid_expry=10, senderid_list=test_list, ) self.senderIDs = SenderIDs(settings) eq_(self.senderIDs.conn.get_bucket(settings.get("s3_bucket")). get_key('senderids').get_contents_as_string(), json.dumps(settings.get("senderid_list"))) eq_(self.senderIDs.senderIDs(), settings.get("senderid_list")) # choose_ID may modify the record in memory adding a field. got = self.senderIDs.choose_ID() ok_(got.get('senderID') in settings.get("senderid_list").keys()) ok_(got.get('auth') == settings.get("senderid_list")[got.get('senderID')]['auth']) self.senderIDs._expry = 0 def test_ensureCreated(self): settings = dict( s3_bucket=TEST_BUCKET, senderid_expry=0, senderid_list=test_list, ) self.senderIDs = SenderIDs(settings) oldConn = self.senderIDs.conn self.senderIDs.conn = Mock() self.senderIDs.conn.get_bucket.side_effect = \ [S3ResponseError(404, "Not Found", ""), None] self.senderIDs._create = Mock() def handle_finish(*args): ok_(self.senderIDs._create.called) self.senderIDs.conn = oldConn d = self.senderIDs._refresh() d.addBoth(handle_finish) return d def test_update(self): settings = dict( s3_bucket=TEST_BUCKET, senderid_expry=0, senderid_list=test_list, ) senderIDs = SenderIDs(settings) update = {"test789": {"auth": "ghi"}} senderIDs.update(update) eq_(senderIDs.conn.get_bucket(settings.get("s3_bucket")). get_key('senderids').get_contents_as_string(), json.dumps(update)) return def test_bad_update(self): settings = dict( s3_bucket=TEST_BUCKET, senderid_expry=0, senderid_list=test_list, ) self.senderIDs = SenderIDs(settings) update = {} d = self.senderIDs.update(update) eq_(d, None) eq_(self.senderIDs._senderIDs, test_list) self.senderIDs.update([123]) eq_(d, None) eq_(self.senderIDs._senderIDs, test_list) self.senderIDs.conn.create_bucket(TEST_BUCKET) # Try a valid, but incorrectly formatted set of senderIDs tkey = Key(self.senderIDs.conn.get_bucket(TEST_BUCKET)) tkey.key = self.senderIDs.KEYNAME tkey.set_contents_from_string("[123,456]") self.senderIDs._update_senderIDs() eq_(self.senderIDs._senderIDs, test_list) return def test_get_record(self): settings = dict( s3_bucket=TEST_BUCKET, senderid_expry=0, senderid_list=test_list, ) self.senderIDs = SenderIDs(settings) fetch = self.senderIDs.get_ID('test123') eq_(fetch, {"senderID": "test123", "auth": "abc"}) return self.senderIDs.stop() def test_get_norecord(self): settings = dict( s3_bucket=TEST_BUCKET, senderid_expry=0, ) self.senderIDs = SenderIDs(settings) fetch = self.senderIDs.choose_ID() eq_(fetch, None) return def test_refresh(self): settings = dict( s3_bucket=TEST_BUCKET, senderid_expry=0, senderid_list=test_list, ) self.senderIDs = SenderIDs(settings) self.senderIDs._senderIDs = {} self.senderIDs._expry = 0 twisted.internet.base.DelayedCall.debug = True def finish_handler(*args): eq_(self.senderIDs._senderIDs, test_list) d = self.senderIDs._refresh() d.addBoth(finish_handler) return d @patch("autopush.senderids.LoopingCall", spec=twisted.internet.task.LoopingCall) def test_start(self, fts): settings = dict( s3_bucket=TEST_BUCKET, senderid_expry=0, senderid_list=test_list, ) self.senderIDs = SenderIDs(settings) self.senderIDs.start() ok_(self.senderIDs.service.start.called) fts.running = True self.senderIDs.stop() ok_(self.senderIDs.service.stop.called)
class GCMRouter(object): """GCM Router Implementation""" log = Logger() gcm = None dryRun = 0 collapseKey = "simplepush" def __init__(self, ap_settings, router_conf): """Create a new GCM router and connect to GCM""" self.config = router_conf self.min_ttl = router_conf.get("ttl", 60) self.dryRun = router_conf.get("dryrun", False) self.collapseKey = router_conf.get("collapseKey", "simplepush") self.senderIDs = router_conf.get("senderIDs") if not self.senderIDs: self.senderIDs = SenderIDs(router_conf) try: senderID = self.senderIDs.choose_ID() self.gcm = gcmclient.GCM(senderID.get("auth")) except: raise IOError("GCM Bridge not initiated in main") self.log.debug("Starting GCM router...") def check_token(self, token): if token not in self.senderIDs.senderIDs(): return (False, self.senderIDs.choose_ID().get('senderID')) return (True, token) def amend_msg(self, msg, data=None): if data is not None: msg["senderid"] = data.get('creds', {}).get('senderID') return msg def register(self, uaid, router_data, router_token=None, *kwargs): """Validate that the GCM Instance Token is in the ``router_data``""" if "token" not in router_data: raise self._error("connect info missing GCM Instance 'token'", status=401) # Assign a senderid router_data["creds"] = self.senderIDs.get_ID(router_token) return router_data def route_notification(self, notification, uaid_data): """Start the GCM notification routing, returns a deferred""" router_data = uaid_data["router_data"] # Kick the entire notification routing off to a thread return deferToThread(self._route, notification, router_data) def _route(self, notification, router_data): """Blocking GCM call to route the notification""" data = {"chid": notification.channel_id} # Payload data is optional. The endpoint handler validates that the # correct encryption headers are included with the data. if notification.data: mdata = self.config.get('max_data', 4096) if len(notification.data) > 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" % (len(notification.data) - mdata), 413, errno=104) data['body'] = notification.data data['con'] = notification.headers['content-encoding'] 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 GCM instance tokens (specified during # registration. router_ttl = notification.ttl or 0 payload = gcmclient.JSONMessage( registration_ids=[router_data.get("token")], collapse_key=self.collapseKey, time_to_live=max(self.min_ttl, router_ttl), dry_run=self.dryRun or ("dryrun" in router_data), data=data, ) creds = router_data.get("creds", {"senderID": "missing id"}) try: self.gcm.api_key = creds["auth"] result = self.gcm.send(payload) except KeyError: raise self._error( "Server error, missing bridge credentials " + "for %s" % creds.get("senderID"), 500) except gcmclient.GCMAuthenticationError, e: raise self._error("Authentication Error: %s" % e, 500) except Exception, e: raise self._error("Unhandled exception in GCM Routing: %s" % e, 500)