Exemple #1
0
 def setUp(self):
     settings = AutopushSettings(
         hostname="localhost",
         statsd_host=None,
     )
     apns_config = {'cert_file': 'fake.cert', 'key_file': 'fake.key'}
     self.mock_apns = Mock(spec=apns.APNs)
     self.router = APNSRouter(settings, apns_config)
     self.router.apns = self.mock_apns
     self.notif = Notification(10, "data", dummy_chid, None, 200)
     self.router_data = dict(router_data=dict(token="connect_data"))
Exemple #2
0
 def setUp(self):
     from twisted.logger import Logger
     settings = AutopushSettings(
         hostname="localhost",
         statsd_host=None,
     )
     apns_config = {'cert_file': 'fake.cert', 'key_file': 'fake.key'}
     self.mock_apns = Mock(spec=apns.APNs)
     self.router = APNSRouter(settings, apns_config)
     self.router.apns = self.mock_apns
     self.router.log = Mock(spec=Logger)
     self.headers = {"content-encoding": "aesgcm",
                     "encryption": "test",
                     "encryption-key": "test"}
     self.notif = Notification(10, "q60d6g", dummy_chid, self.headers,
                               200)
     self.router_data = dict(router_data=dict(token="connect_data"))
 def setUp(self, mt, mc):
     from twisted.logger import Logger
     conf = AutopushConfig(
         hostname="localhost",
         statsd_host=None,
     )
     apns_config = {
         'firefox': {
             'cert': 'fake.cert',
             'key': 'fake.key',
             'topic': 'com.example.SomeApp',
             'max_connections': 2,
         }
     }
     self.mock_connection = mc
     mc.return_value = mc
     self.metrics = metrics = Mock(spec=SinkMetrics)
     self.router = APNSRouter(conf, apns_config, metrics)
     self.mock_response = Mock()
     self.mock_response.status = 200
     mc.get_response.return_value = self.mock_response
     # toss the existing connection
     try:
         self.router.apns['firefox'].connections.pop()
     except IndexError:  # pragma nocover
         pass
     self.router.apns['firefox'].connections.append(self.mock_connection)
     self.router.apns['firefox'].log = Mock(spec=Logger)
     self.headers = {
         "content-encoding": "aesgcm",
         "encryption": "test",
         "encryption-key": "test"
     }
     self.notif = WebPushNotification(
         uaid=uuid.UUID(dummy_uaid),
         channel_id=uuid.UUID(dummy_chid),
         data="q60d6g",
         headers=self.headers,
         ttl=200,
         message_id=10,
     )
     self.notif.cleanup_headers()
     self.router_data = dict(
         router_data=dict(token="connect_data", rel_channel="firefox"))
Exemple #4
0
 def setUp(self):
     settings = AutopushSettings(
         hostname="localhost",
         statsd_host=None,
     )
     apns_config = {'cert_file': 'fake.cert', 'key_file': 'fake.key'}
     self.mock_apns = Mock(spec=apns.APNs)
     self.router = APNSRouter(settings, apns_config)
     self.router.apns = self.mock_apns
     self.notif = Notification(10, "data", dummy_chid, None, 200)
     self.router_data = dict(router_data=dict(token="connect_data"))
Exemple #5
0
class APNSRouterTestCase(unittest.TestCase):
    def setUp(self):
        settings = AutopushSettings(
            hostname="localhost",
            statsd_host=None,
        )
        apns_config = {'cert_file': 'fake.cert', 'key_file': 'fake.key'}
        self.mock_apns = Mock(spec=apns.APNs)
        self.router = APNSRouter(settings, apns_config)
        self.router.apns = self.mock_apns
        self.notif = Notification(10, "data", dummy_chid, None, 200)
        self.router_data = dict(router_data=dict(token="connect_data"))

    def test_register(self):
        result = self.router.register("uaid", {"token": "connect_data"})
        eq_(result, {"token": "connect_data"})

    def test_register_bad(self):
        self.assertRaises(RouterException, self.router.register, "uaid", {})

    def test_route_notification(self):
        d = self.router.route_notification(self.notif, self.router_data)

        def check_results(result):
            ok_(isinstance(result, RouterResponse))
            assert(self.mock_apns.gateway_server.send_notification.called)

        d.addCallback(check_results)
        return d

    def test_message_pruning(self):
        now = int(time.time())
        self.router.messages = {now: {'token': 'dump', 'payload': {}},
                                now-60: {'token': 'dump', 'payload': {}}}
        d = self.router.route_notification(self.notif, self.router_data)

        def check_results(result):
            ok_(isinstance(result, RouterResponse))
            assert(self.mock_apns.gateway_server.send_notification.called)
            eq_(len(self.router.messages), 1)
        d.addCallback(check_results)
        return d

    def test_response_listener_with_success(self):
        self.router.messages = {1: {'token': 'dump', 'payload': {}}}
        self.router._error(dict(status=0, identifier=1))
        eq_(len(self.router.messages), 0)

    def test_response_listener_with_nonretryable_error(self):
        self.router.messages = {1: {'token': 'dump', 'payload': {}}}
        self.router._error(dict(status=2, identifier=1))
        eq_(len(self.router.messages), 1)

    def test_response_listener_with_retryable_existing_message(self):
        self.router.messages = {1: {'token': 'dump', 'payload': {}}}
        # Mock out the _connect call to be harmless
        self.router._connect = Mock()
        self.router._error(dict(status=1, identifier=1))
        eq_(len(self.router.messages), 1)
        assert(self.router.apns.gateway_server.send_notification.called)

    def test_response_listener_with_retryable_non_existing_message(self):
        self.router.messages = {1: {'token': 'dump', 'payload': {}}}
        self.router._error(dict(status=1, identifier=10))
        eq_(len(self.router.messages), 1)

    def test_ammend(self):
        resp = {"key": "value"}
        eq_(resp, self.router.amend_msg(resp))

    def test_check_token(self):
        (t, v) = self.router.check_token("")
        ok_(t)
class APNSRouterTestCase(unittest.TestCase):
    def _waitfor(self, func):
        times = 0
        while not func():  # pragma: nocover
            time.sleep(1)
            times += 1
            if times > 9:
                break

    @patch('autopush.router.apns2.HTTP20Connection',
           spec=hyper.HTTP20Connection)
    @patch('hyper.tls', spec=hyper.tls)
    def setUp(self, mt, mc):
        from twisted.logger import Logger
        conf = AutopushConfig(
            hostname="localhost",
            statsd_host=None,
        )
        apns_config = {
            'firefox': {
                'cert': 'fake.cert',
                'key': 'fake.key',
                'topic': 'com.example.SomeApp',
                'max_connections': 2,
            }
        }
        self.mock_connection = mc
        mc.return_value = mc
        self.metrics = metrics = Mock(spec=SinkMetrics)
        self.router = APNSRouter(conf, apns_config, metrics)
        self.mock_response = Mock()
        self.mock_response.status = 200
        mc.get_response.return_value = self.mock_response
        # toss the existing connection
        try:
            self.router.apns['firefox'].connections.pop()
        except IndexError:  # pragma nocover
            pass
        self.router.apns['firefox'].connections.append(self.mock_connection)
        self.router.apns['firefox'].log = Mock(spec=Logger)
        self.headers = {
            "content-encoding": "aesgcm",
            "encryption": "test",
            "encryption-key": "test"
        }
        self.notif = WebPushNotification(
            uaid=uuid.UUID(dummy_uaid),
            channel_id=uuid.UUID(dummy_chid),
            data="q60d6g",
            headers=self.headers,
            ttl=200,
            message_id=10,
        )
        self.notif.cleanup_headers()
        self.router_data = dict(
            router_data=dict(token="connect_data", rel_channel="firefox"))

    def test_register(self):
        router_data = {"token": "connect_data"}
        self.router.register("uaid", router_data=router_data, app_id="firefox")
        assert router_data == {
            "rel_channel": "firefox",
            "token": "connect_data"
        }

    def test_extended_register(self):
        router_data = {
            "token": "connect_data",
            "aps": {
                "foo": "bar",
                "gorp": "baz"
            }
        }
        self.router.register("uaid", router_data=router_data, app_id="firefox")
        assert router_data == {
            "rel_channel": "firefox",
            "token": "connect_data",
            "aps": {
                "foo": "bar",
                "gorp": "baz"
            }
        }

    def test_register_bad(self):
        with pytest.raises(RouterException):
            self.router.register("uaid", router_data={}, app_id="firefox")

    def test_register_bad_channel(self):
        with pytest.raises(RouterException):
            self.router.register("uaid",
                                 router_data={"token": "connect_data"},
                                 app_id="unknown")

    @inlineCallbacks
    def test_connection_error(self):
        from hyper.http20.exceptions import ConnectionError

        def raiser(*args, **kwargs):
            raise ConnectionError("oops")

        self.router.apns['firefox'].connections[1].request = Mock(
            side_effect=raiser)

        with pytest.raises(RouterException) as ex:
            yield self.router.route_notification(self.notif, self.router_data)

        assert ex.value.response_body == ('APNS returned an error '
                                          'processing request')
        assert ex.value.status_code == 502
        self.flushLoggedErrors()

    @inlineCallbacks
    def test_connection_fail_error(self):
        def raiser(*args, **kwargs):
            error = socket.error()
            error.errno = socket.errno.EPIPE
            raise error

        self.router.apns['firefox'].connections[1].request = Mock(
            side_effect=raiser)

        with pytest.raises(RouterException) as ex:
            yield self.router.route_notification(self.notif, self.router_data)

        assert ex.value.response_body == "APNS returned an error processing " \
                                         "request"
        assert ex.value.status_code == 502
        self.flushLoggedErrors()

    @inlineCallbacks
    def test_route_notification(self):
        result = yield self.router.route_notification(self.notif,
                                                      self.router_data)
        yield self._waitfor(
            lambda: self.mock_connection.request.called is True)

        assert isinstance(result, RouterResponse)
        assert self.mock_connection.request.called
        body = self.mock_connection.request.call_args[1]
        body_json = json.loads(body['body'])
        assert 'chid' in body_json
        # The ChannelID is a UUID4, and unpredictable.
        del (body_json['chid'])
        assert body_json == {
            "body": "q60d6g",
            "enc": "test",
            "ver": 10,
            "aps": {
                "mutable-content": 1,
                "alert": {
                    "loc-key": "SentTab.NoTabArrivingNotification.body",
                    "title-loc-key": "SentTab.NoTabArrivingNotification.title",
                },
            },
            "enckey": "test",
            "con": "aesgcm",
        }

    @inlineCallbacks
    def test_route_notification_complex(self):
        router_data = dict(
            router_data=dict(token="connect_data",
                             rel_channel="firefox",
                             aps=dict(string="String",
                                      array=['a', 'b', 'c'],
                                      number=decimal.Decimal(4))))
        result = yield self.router.route_notification(self.notif, router_data)
        yield self._waitfor(
            lambda: self.mock_connection.request.called is True)
        assert isinstance(result, RouterResponse)
        assert self.mock_connection.request.called
        body = self.mock_connection.request.call_args[1]
        body_json = json.loads(body['body'])
        assert body_json['aps']['number'] == 4
        assert body_json['aps']['string'] == 'String'

    @inlineCallbacks
    def test_route_low_priority_notification(self):
        """low priority and empty apns_ids are not yet used, but may feature
        when priorty work is done."""
        apns2 = self.router.apns['firefox']
        exp = int(time.time() + 300)
        yield apns2.send("abcd0123", {}, 'apnsid', priority=False, exp=exp)
        yield self._waitfor(
            lambda: self.mock_connection.request.called is True)
        assert self.mock_connection.request.called
        body = self.mock_connection.request.call_args[1]
        headers = body['headers']
        assert headers == {
            'apns-expiration': str(exp),
            'apns-topic': 'com.example.SomeApp',
            'apns-priority': '5',
            'apns-id': 'apnsid'
        }

    @inlineCallbacks
    def test_bad_send(self):
        self.mock_response.status = 400
        self.mock_response.read.return_value = json.dumps({'reason': 'boo'})
        with pytest.raises(RouterException) as ex:
            yield self.router.route_notification(self.notif, self.router_data)
        assert isinstance(ex.value, RouterException)
        assert ex.value.status_code == 502
        assert ex.value.message == 'APNS Transmit Error 400:boo'
        assert ex.value.response_body == (
            'APNS could not process your message boo')

    @inlineCallbacks
    def test_fail_send(self):
        def throw(*args, **kwargs):
            raise HTTP20Error("oops")

        self.router.apns['firefox'].connections[0].request.side_effect = throw
        with pytest.raises(RouterException) as ex:
            yield self.router.route_notification(self.notif, self.router_data)
        assert isinstance(ex.value, RouterException)
        assert ex.value.status_code == 502
        assert ex.value.message == "Server error"
        assert ex.value.response_body == 'APNS returned an error ' \
                                         'processing request'
        assert self.metrics.increment.called
        assert self.metrics.increment.call_args[0][0] == \
            'notification.bridge.connection.error'
        self.flushLoggedErrors()

    @inlineCallbacks
    def test_fail_send_bad_write_retry(self):
        def throw(*args, **kwargs):
            raise ssl.SSLError(ssl.SSL_ERROR_SSL,
                               "[SSL: BAD_WRITE_RETRY] bad write retry")

        self.router.apns['firefox'].connections[0].request.side_effect = throw
        with pytest.raises(RouterException) as ex:
            yield self.router.route_notification(self.notif, self.router_data)
        assert isinstance(ex.value, RouterException)
        assert ex.value.status_code == 502
        assert ex.value.message == "Server error"
        assert ex.value.response_body == 'APNS returned an error ' \
                                         'processing request'
        assert self.metrics.increment.called
        assert self.metrics.increment.call_args[0][0] == \
            'notification.bridge.connection.error'
        self.flushLoggedErrors()

    def test_too_many_connections(self):
        rr = self.router.apns['firefox']
        with pytest.raises(RouterException) as ex:
            while True:
                rr._get_connection()

        assert isinstance(ex.value, RouterException)
        assert ex.value.status_code == 503
        assert ex.value.message == "Too many APNS requests, " \
                                   "increase pool from 2"
        assert ex.value.response_body == "APNS busy, please retry"

    def test_amend(self):
        resp = {"key": "value"}
        expected = resp.copy()
        self.router.amend_endpoint_response(resp, {})
        assert resp == expected

    def test_route_crypto_key(self):
        headers = {
            "content-encoding": "aesgcm",
            "encryption": "test",
            "crypto-key": "test"
        }
        self.notif = WebPushNotification(
            uaid=uuid.UUID(dummy_uaid),
            channel_id=uuid.UUID(dummy_chid),
            data="q60d6g",
            headers=headers,
            ttl=200,
            message_id=10,
        )
        self.notif.cleanup_headers()
        d = self.router.route_notification(self.notif, self.router_data)

        def check_results(result):
            assert isinstance(result, RouterResponse)
            assert result.status_code == 201
            assert result.logged_status == 200
            assert "TTL" in result.headers
            assert self.mock_connection.called

        d.addCallback(check_results)
        return d
Exemple #7
0
class APNSRouterTestCase(unittest.TestCase):
    def setUp(self):
        settings = AutopushSettings(
            hostname="localhost",
            statsd_host=None,
        )
        apns_config = {'cert_file': 'fake.cert', 'key_file': 'fake.key'}
        self.mock_apns = Mock(spec=apns.APNs)
        self.router = APNSRouter(settings, apns_config)
        self.router.apns = self.mock_apns
        self.notif = Notification(10, "data", dummy_chid, None, 200)
        self.router_data = dict(router_data=dict(token="connect_data"))

    def test_register(self):
        result = self.router.register("uaid", {"token": "connect_data"})
        eq_(result, {"token": "connect_data"})

    def test_register_bad(self):
        self.assertRaises(RouterException, self.router.register, "uaid", {})

    def test_route_notification(self):
        d = self.router.route_notification(self.notif, self.router_data)

        def check_results(result):
            ok_(isinstance(result, RouterResponse))
            assert(self.mock_apns.gateway_server.send_notification.called)

        d.addCallback(check_results)
        return d

    def test_message_pruning(self):
        now = int(time.time())
        self.router.messages = {now: {'token': 'dump', 'payload': {}},
                                now-60: {'token': 'dump', 'payload': {}}}
        d = self.router.route_notification(self.notif, self.router_data)

        def check_results(result):
            ok_(isinstance(result, RouterResponse))
            assert(self.mock_apns.gateway_server.send_notification.called)
            eq_(len(self.router.messages), 1)
        d.addCallback(check_results)
        return d

    def test_response_listener_with_success(self):
        self.router.messages = {1: {'token': 'dump', 'payload': {}}}
        self.router._error(dict(status=0, identifier=1))
        eq_(len(self.router.messages), 0)

    def test_response_listener_with_nonretryable_error(self):
        self.router.messages = {1: {'token': 'dump', 'payload': {}}}
        self.router._error(dict(status=2, identifier=1))
        eq_(len(self.router.messages), 1)

    def test_response_listener_with_retryable_existing_message(self):
        self.router.messages = {1: {'token': 'dump', 'payload': {}}}
        # Mock out the _connect call to be harmless
        self.router._connect = Mock()
        self.router._error(dict(status=1, identifier=1))
        eq_(len(self.router.messages), 1)
        assert(self.router.apns.gateway_server.send_notification.called)

    def test_response_listener_with_retryable_non_existing_message(self):
        self.router.messages = {1: {'token': 'dump', 'payload': {}}}
        self.router._error(dict(status=1, identifier=10))
        eq_(len(self.router.messages), 1)

    def test_ammend(self):
        resp = {"key": "value"}
        eq_(resp, self.router.amend_msg(resp))
Exemple #8
0
class APNSRouterTestCase(unittest.TestCase):
    def setUp(self):
        from twisted.logger import Logger
        settings = AutopushSettings(
            hostname="localhost",
            statsd_host=None,
        )
        apns_config = {'cert_file': 'fake.cert', 'key_file': 'fake.key'}
        self.mock_apns = Mock(spec=apns.APNs)
        self.router = APNSRouter(settings, apns_config)
        self.router.apns = self.mock_apns
        self.router.log = Mock(spec=Logger)
        self.headers = {"content-encoding": "aesgcm",
                        "encryption": "test",
                        "encryption-key": "test"}
        self.notif = Notification(10, "q60d6g", dummy_chid, self.headers,
                                  200)
        self.router_data = dict(router_data=dict(token="connect_data"))

    def test_register(self):
        result = self.router.register("uaid", {"token": "connect_data"})
        eq_(result, {"token": "connect_data"})

    def test_register_bad(self):
        self.assertRaises(RouterException, self.router.register, "uaid", {})

    def test_route_notification(self):
        d = self.router.route_notification(self.notif, self.router_data)

        def check_results(result):
            ok_(isinstance(result, RouterResponse))
            assert(self.mock_apns.gateway_server.send_notification.called)

        d.addCallback(check_results)
        return d

    def test_message_pruning(self):
        now = int(time.time())
        self.router.messages = {now: {'token': 'dump', 'payload': {}},
                                now-60: {'token': 'dump', 'payload': {}}}
        d = self.router.route_notification(self.notif, self.router_data)

        def check_results(result):
            ok_(isinstance(result, RouterResponse))
            assert(self.mock_apns.gateway_server.send_notification.called)
            eq_(len(self.router.messages), 1)

            payload = self.router.messages[now]['payload']
            eq_(payload.alert, 'SimplePush')

            custom = payload.custom
            eq_(custom['Msg'], self.notif.data)
            eq_(custom['Ver'], self.notif.version)
            eq_(custom['Con'], 'aesgcm')
            eq_(custom['Enc'], 'test')
            eq_(custom['Enckey'], 'test')
            eq_(custom['Chid'], self.notif.channel_id)
            ok_('Cryptokey' not in custom)

        d.addCallback(check_results)
        return d

    def test_response_listener_with_success(self):
        self.router.messages = {1: {'token': 'dump', 'payload': {}}}
        self.router._error(dict(status=0, identifier=1))
        eq_(len(self.router.messages), 0)

    def test_response_listener_with_nonretryable_error(self):
        self.router.messages = {1: {'token': 'dump', 'payload': {}}}
        self.router._error(dict(status=2, identifier=1))
        eq_(len(self.router.messages), 1)

    def test_response_listener_with_retryable_existing_message(self):
        self.router.messages = {1: {'token': 'dump', 'payload': {}}}
        # Mock out the _connect call to be harmless
        self.router._connect = Mock()
        self.router._error(dict(status=1, identifier=1))
        eq_(len(self.router.messages), 1)
        assert(self.router.apns.gateway_server.send_notification.called)

    def test_response_listener_with_retryable_non_existing_message(self):
        self.router.messages = {1: {'token': 'dump', 'payload': {}}}
        self.router._error(dict(status=1, identifier=10))
        eq_(len(self.router.messages), 1)

    def test_ammend(self):
        resp = {"key": "value"}
        eq_(resp, self.router.amend_msg(resp))

    def test_check_token(self):
        (t, v) = self.router.check_token("")
        ok_(t)

    def test_route_crypto_key(self):
        headers = {"content-encoding": "aesgcm",
                   "encryption": "test",
                   "crypto-key": "test"}
        self.notif = Notification(10, "q60d6g", dummy_chid, headers, 200)
        now = int(time.time())
        self.router.messages = {now: {'token': 'dump', 'payload': {}},
                                now-60: {'token': 'dump', 'payload': {}}}
        d = self.router.route_notification(self.notif, self.router_data)

        def check_results(result):
            ok_(isinstance(result, RouterResponse))
            assert(self.mock_apns.gateway_server.send_notification.called)
            eq_(len(self.router.messages), 1)

            payload = self.router.messages[now]['payload']
            eq_(payload.alert, 'SimplePush')

            custom = payload.custom
            eq_(custom['Msg'], self.notif.data)
            eq_(custom['Ver'], self.notif.version)
            eq_(custom['Con'], 'aesgcm')
            eq_(custom['Enc'], 'test')
            eq_(custom['Cryptokey'], 'test')
            eq_(custom['Chid'], self.notif.channel_id)
            ok_('Enckey' not in custom)

        d.addCallback(check_results)
        return d
Exemple #9
0
    def __init__(
        self,
        crypto_key=None,
        datadog_api_key=None,
        datadog_app_key=None,
        datadog_flush_interval=None,
        hostname=None,
        port=None,
        router_scheme=None,
        router_hostname=None,
        router_port=None,
        endpoint_scheme=None,
        endpoint_hostname=None,
        endpoint_port=None,
        router_conf={},
        router_tablename="router",
        router_read_throughput=5,
        router_write_throughput=5,
        storage_tablename="storage",
        storage_read_throughput=5,
        storage_write_throughput=5,
        message_tablename="message",
        message_read_throughput=5,
        message_write_throughput=5,
        statsd_host="localhost",
        statsd_port=8125,
        resolve_hostname=False,
        max_data=4096,
        # Reflected up from UDP Router
        wake_timeout=0,
        env='development',
        enable_cors=False,
        s3_bucket=DEFAULT_BUCKET,
        senderid_expry=SENDERID_EXPRY,
        senderid_list={},
        hello_timeout=0,
    ):
        """Initialize the Settings object

        Upon creation, the HTTP agent will initialize, all configured routers
        will be setup and started, logging will be started, and the database
        will have a preflight check done.

        """
        # Use a persistent connection pool for HTTP requests.
        pool = HTTPConnectionPool(reactor)
        self.agent = Agent(reactor, connectTimeout=5, pool=pool)

        # Metrics setup
        if datadog_api_key:
            self.metrics = DatadogMetrics(
                api_key=datadog_api_key,
                app_key=datadog_app_key,
                flush_interval=datadog_flush_interval)
        elif statsd_host:
            self.metrics = TwistedMetrics(statsd_host, statsd_port)
        else:
            self.metrics = SinkMetrics()
        if not crypto_key:
            crypto_key = [Fernet.generate_key()]
        if not isinstance(crypto_key, list):
            crypto_key = [crypto_key]
        self.update(crypto_key=crypto_key)
        self.crypto_key = crypto_key

        self.max_data = max_data
        self.clients = {}

        # Setup hosts/ports/urls
        default_hostname = socket.gethostname()
        self.hostname = hostname or default_hostname
        if resolve_hostname:
            self.hostname = resolve_ip(self.hostname)

        self.port = port
        self.endpoint_hostname = endpoint_hostname or self.hostname
        self.router_hostname = router_hostname or self.hostname

        self.router_conf = router_conf
        self.router_url = canonical_url(router_scheme or 'http',
                                        self.router_hostname, router_port)

        self.endpoint_url = canonical_url(endpoint_scheme or 'http',
                                          self.endpoint_hostname,
                                          endpoint_port)

        # Database objects
        self.router_table = get_router_table(router_tablename,
                                             router_read_throughput,
                                             router_write_throughput)
        self.storage_table = get_storage_table(storage_tablename,
                                               storage_read_throughput,
                                               storage_write_throughput)
        self.message_table = get_message_table(message_tablename,
                                               message_read_throughput,
                                               message_write_throughput)
        self.storage = Storage(self.storage_table, self.metrics)
        self.router = Router(self.router_table, self.metrics)
        self.message = Message(self.message_table, self.metrics)

        # Run preflight check
        preflight_check(self.storage, self.router)

        # CORS
        self.cors = enable_cors

        # Force timeout in idle seconds
        self.wake_timeout = wake_timeout

        # Setup the routers
        self.routers = {}
        self.routers["simplepush"] = SimpleRouter(
            self, router_conf.get("simplepush"))
        self.routers["webpush"] = WebPushRouter(self, None)
        if 'apns' in router_conf:
            self.routers["apns"] = APNSRouter(self, router_conf["apns"])
        if 'gcm' in router_conf:
            self.routers["gcm"] = GCMRouter(self, router_conf["gcm"])

        # Env
        self.env = env

        self.hello_timeout = hello_timeout
Exemple #10
0
class APNSRouterTestCase(unittest.TestCase):
    def _waitfor(self, func):
        times = 0
        while not func():  # pragma: nocover
            time.sleep(1)
            times += 1
            if times > 9:
                break

    @patch('autopush.router.apns2.HTTP20Connection',
           spec=hyper.HTTP20Connection)
    @patch('hyper.tls', spec=hyper.tls)
    def setUp(self, mt, mc):
        from twisted.logger import Logger
        settings = AutopushSettings(
            hostname="localhost",
            statsd_host=None,
        )
        apns_config = {
            'firefox': {
                'cert': 'fake.cert',
                'key': 'fake.key',
                'topic': 'com.example.SomeApp',
                'max_connections': 2,
            }
        }
        self.mock_connection = mc
        mc.return_value = mc
        self.router = APNSRouter(settings, apns_config)
        self.mock_response = Mock()
        self.mock_response.status = 200
        mc.get_response.return_value = self.mock_response
        # toss the existing connection
        try:
            self.router.apns['firefox'].connections.pop()
        except IndexError:  # pragma nocover
            pass
        self.router.apns['firefox'].connections.append(self.mock_connection)
        self.router.apns['firefox'].log = Mock(spec=Logger)
        self.headers = {
            "content-encoding": "aesgcm",
            "encryption": "test",
            "encryption-key": "test"
        }
        self.notif = WebPushNotification(
            uaid=uuid.UUID(dummy_uaid),
            channel_id=uuid.UUID(dummy_chid),
            data="q60d6g",
            headers=self.headers,
            ttl=200,
            message_id=10,
        )
        self.notif.cleanup_headers()
        self.router_data = dict(
            router_data=dict(token="connect_data", rel_channel="firefox"))

    def test_register(self):
        router_data = {"token": "connect_data"}
        self.router.register("uaid", router_data=router_data, app_id="firefox")
        eq_(router_data, {"rel_channel": "firefox", "token": "connect_data"})

    def test_register_bad(self):
        with assert_raises(RouterException):
            self.router.register("uaid", router_data={}, app_id="firefox")

    def test_register_bad_channel(self):
        with assert_raises(RouterException):
            self.router.register("uaid",
                                 router_data={"token": "connect_data"},
                                 app_id="unknown")

    @inlineCallbacks
    def test_connection_error(self):
        from hyper.http20.exceptions import ConnectionError

        def raiser(*args, **kwargs):
            raise ConnectionError("oops")

        self.router.apns['firefox'].connections[1].request = Mock(
            side_effect=raiser)

        with assert_raises(RouterException) as e:
            yield self.router.route_notification(self.notif, self.router_data)

        eq_(e.exception.response_body, 'APNS returned an error '
            'processing request')
        eq_(e.exception.status_code, 502)
        self.flushLoggedErrors()

    @inlineCallbacks
    def test_route_notification(self):
        result = yield self.router.route_notification(self.notif,
                                                      self.router_data)
        yield self._waitfor(
            lambda: self.mock_connection.request.called is True)

        ok_(isinstance(result, RouterResponse))
        ok_(self.mock_connection.request.called)
        body = self.mock_connection.request.call_args[1]
        body_json = json.loads(body['body'])
        ok_('chid' in body_json)
        # The ChannelID is a UUID4, and unpredictable.
        del (body_json['chid'])
        eq_(
            body_json, {
                "body": "q60d6g",
                "enc": "test",
                "ver": 10,
                "aps": {
                    "content-available": 1,
                },
                "enckey": "test",
                "con": "aesgcm",
            })

    @inlineCallbacks
    def test_route_low_priority_notification(self):
        """low priority and empty apns_ids are not yet used, but may feature
        when priorty work is done."""
        apns2 = self.router.apns['firefox']
        exp = int(time.time() + 300)
        yield apns2.send("abcd0123", {}, 'apnsid', priority=False, exp=exp)
        yield self._waitfor(
            lambda: self.mock_connection.request.called is True)
        ok_(self.mock_connection.request.called)
        body = self.mock_connection.request.call_args[1]
        headers = body['headers']
        eq_(
            headers, {
                'apns-expiration': str(exp),
                'apns-topic': 'com.example.SomeApp',
                'apns-priority': '5',
                'apns-id': 'apnsid'
            })

    @inlineCallbacks
    def test_bad_send(self):
        self.mock_response.status = 400
        self.mock_response.read.return_value = json.dumps({'reason': 'boo'})
        with assert_raises(RouterException) as ex:
            yield self.router.route_notification(self.notif, self.router_data)
        ok_(isinstance(ex.exception, RouterException))
        eq_(ex.exception.status_code, 502)
        eq_(ex.exception.message, 'APNS Transmit Error 400:boo')
        eq_(ex.exception.response_body, 'APNS could not process your '
            'message boo')

    @inlineCallbacks
    def test_fail_send(self):
        def throw(*args, **kwargs):
            raise HTTP20Error("oops")

        self.router.apns['firefox'].connections[0].request.side_effect = throw
        with assert_raises(RouterException) as ex:
            yield self.router.route_notification(self.notif, self.router_data)
        ok_(isinstance(ex.exception, RouterException))
        eq_(ex.exception.status_code, 502)
        eq_(ex.exception.message, "Server error")
        eq_(ex.exception.response_body, 'APNS returned an error processing '
            'request')
        self.flushLoggedErrors()

    def test_too_many_connections(self):
        rr = self.router.apns['firefox']
        with assert_raises(RouterException) as ex:
            while True:
                rr._get_connection()

        ok_(isinstance(ex.exception, RouterException))
        eq_(ex.exception.status_code, 503)
        eq_(ex.exception.message, "Too many APNS requests, "
            "increase pool from 2")
        eq_(ex.exception.response_body, "APNS busy, please retry")

    def test_amend(self):
        resp = {"key": "value"}
        expected = resp.copy()
        self.router.amend_endpoint_response(resp, {})
        eq_(resp, expected)

    def test_route_crypto_key(self):
        headers = {
            "content-encoding": "aesgcm",
            "encryption": "test",
            "crypto-key": "test"
        }
        self.notif = WebPushNotification(
            uaid=uuid.UUID(dummy_uaid),
            channel_id=uuid.UUID(dummy_chid),
            data="q60d6g",
            headers=headers,
            ttl=200,
            message_id=10,
        )
        self.notif.cleanup_headers()
        d = self.router.route_notification(self.notif, self.router_data)

        def check_results(result):
            ok_(isinstance(result, RouterResponse))
            eq_(result.status_code, 201)
            eq_(result.logged_status, 200)
            ok_("TTL" in result.headers)
            ok_(self.mock_connection.called)

        d.addCallback(check_results)
        return d
Exemple #11
0
    def __init__(
        self,
        crypto_key=None,
        datadog_api_key=None,
        datadog_app_key=None,
        datadog_flush_interval=None,
        hostname=None,
        port=None,
        router_scheme=None,
        router_hostname=None,
        router_port=None,
        endpoint_scheme=None,
        endpoint_hostname=None,
        endpoint_port=None,
        router_conf=None,
        router_tablename="router",
        router_read_throughput=5,
        router_write_throughput=5,
        storage_tablename="storage",
        storage_read_throughput=5,
        storage_write_throughput=5,
        message_tablename="message",
        message_read_throughput=5,
        message_write_throughput=5,
        statsd_host="localhost",
        statsd_port=8125,
        resolve_hostname=False,
        max_data=4096,
        # Reflected up from UDP Router
        wake_timeout=0,
        env='development',
        enable_cors=False,
        hello_timeout=0,
        bear_hash_key=None,
        preflight_uaid="deadbeef00000000deadbeef00000000",
        ami_id=None,
        client_certs=None,
        msg_limit=100,
        debug=False,
        connect_timeout=0.5,
    ):
        """Initialize the Settings object

        Upon creation, the HTTP agent will initialize, all configured routers
        will be setup and started, logging will be started, and the database
        will have a preflight check done.

        """
        # Use a persistent connection pool for HTTP requests.
        pool = HTTPConnectionPool(reactor)
        if not debug:
            pool._factory = QuietClientFactory

        self.agent = Agent(reactor, connectTimeout=connect_timeout, pool=pool)

        if not crypto_key:
            crypto_key = [Fernet.generate_key()]
        if not isinstance(crypto_key, list):
            crypto_key = [crypto_key]
        self.update(crypto_key=crypto_key)
        self.crypto_key = crypto_key

        if bear_hash_key is None:
            bear_hash_key = []
        if not isinstance(bear_hash_key, list):
            bear_hash_key = [bear_hash_key]
        self.bear_hash_key = bear_hash_key

        self.max_data = max_data
        self.clients = {}

        # Setup hosts/ports/urls
        default_hostname = socket.gethostname()
        self.hostname = hostname or default_hostname
        if resolve_hostname:
            self.hostname = resolve_ip(self.hostname)

        # Metrics setup
        if datadog_api_key:
            self.metrics = DatadogMetrics(
                hostname=self.hostname,
                api_key=datadog_api_key,
                app_key=datadog_app_key,
                flush_interval=datadog_flush_interval,
            )
        elif statsd_host:
            self.metrics = TwistedMetrics(statsd_host, statsd_port)
        else:
            self.metrics = SinkMetrics()

        self.port = port
        self.endpoint_hostname = endpoint_hostname or self.hostname
        self.router_hostname = router_hostname or self.hostname

        if router_conf is None:
            router_conf = {}
        self.router_conf = router_conf
        self.router_url = canonical_url(router_scheme or 'http',
                                        self.router_hostname, router_port)

        self.endpoint_url = canonical_url(endpoint_scheme or 'http',
                                          self.endpoint_hostname,
                                          endpoint_port)
        self.enable_tls_auth = client_certs is not None
        self.client_certs = client_certs

        # Database objects
        self.router_table = get_router_table(router_tablename,
                                             router_read_throughput,
                                             router_write_throughput)
        self.storage_table = get_storage_table(storage_tablename,
                                               storage_read_throughput,
                                               storage_write_throughput)
        self.message_table = get_rotating_message_table(
            message_tablename,
            message_read_throughput=message_read_throughput,
            message_write_throughput=message_write_throughput)
        self._message_prefix = message_tablename
        self.message_limit = msg_limit
        self.storage = Storage(self.storage_table, self.metrics)
        self.router = Router(self.router_table, self.metrics)

        # Used to determine whether a connection is out of date with current
        # db objects. There are three noteworty cases:
        # 1 "Last Month" the table requires a rollover.
        # 2 "This Month" the most common case.
        # 3 "Next Month" where the system will soon be rolling over, but with
        #   timing, some nodes may roll over sooner. Ensuring the next month's
        #   table is present before the switchover is the main reason for this,
        #   just in case some nodes do switch sooner.
        self.create_initial_message_tables()

        # Run preflight check
        preflight_check(self.storage, self.router, preflight_uaid)

        # CORS
        self.cors = enable_cors

        # Force timeout in idle seconds
        self.wake_timeout = wake_timeout

        # Setup the routers
        self.routers = dict()
        self.routers["simplepush"] = SimpleRouter(
            self, router_conf.get("simplepush"))
        self.routers["webpush"] = WebPushRouter(self, None)
        if 'apns' in router_conf:
            self.routers["apns"] = APNSRouter(self, router_conf["apns"])
        if 'gcm' in router_conf:
            self.routers["gcm"] = GCMRouter(self, router_conf["gcm"])

        # Env
        self.env = env

        self.hello_timeout = hello_timeout

        self.ami_id = ami_id

        # Generate messages per legacy rules, only used for testing to
        # generate legacy data.
        self._notification_legacy = False