From 7a960b4ce24e7c9225a053f63a038f3a5c6d28c5 Mon Sep 17 00:00:00 2001 From: Ben Bangert Date: Wed, 9 Mar 2016 17:04:21 -0800 Subject: [PATCH 1/2] chore: update version for 1.13.1 --- autopush/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/autopush/__init__.py b/autopush/__init__.py index 7be266c4..f241adf5 100644 --- a/autopush/__init__.py +++ b/autopush/__init__.py @@ -1 +1 @@ -__version__ = '1.13' # pragma: nocover +__version__ = '1.13.1' # pragma: nocover From 86ba66d46792c8cbd746c706c82390e6823ef686 Mon Sep 17 00:00:00 2001 From: Ben Bangert Date: Wed, 9 Mar 2016 17:04:42 -0800 Subject: [PATCH 2/2] fix: default api_ver to v0 for message endpoint Default the api_ver to v0 for the message endpoint. Previously a value of None could pass in causing earlier endpoints to be incorrectly parsed out. parse_endpoint now throws InvalidTokenExceptions to avoid being caught on accident as a ValueError. Closes #395 --- autopush/endpoint.py | 10 +++-- autopush/exceptions.py | 4 ++ autopush/settings.py | 11 +++--- autopush/tests/test_endpoint.py | 63 ++++++++++++++++-------------- autopush/tests/test_integration.py | 21 +++++++++- 5 files changed, 68 insertions(+), 41 deletions(-) diff --git a/autopush/endpoint.py b/autopush/endpoint.py index 90dba888..60d77fa9 100644 --- a/autopush/endpoint.py +++ b/autopush/endpoint.py @@ -45,6 +45,7 @@ hasher, normalize_id, ) +from autopush.exceptions import InvalidTokenException from autopush.router.interface import RouterException from autopush.utils import ( generate_hash, @@ -268,7 +269,7 @@ def _uaid_not_found_err(self, fail): def _token_err(self, fail): """errBack for token decryption fail""" - fail.trap(InvalidToken, ValueError) + fail.trap(InvalidToken, InvalidTokenException) log.msg("Invalid token", **self._client_info) self._write_response(400, 102) @@ -354,11 +355,11 @@ def _token_valid(self, result, func): function""" info = result.split(":") if len(info) != 3: - raise ValueError("Wrong message token components") + raise InvalidTokenException("Wrong message token components") kind, uaid, chid = info if kind != 'm': - raise ValueError("Wrong message token kind") + raise InvalidTokenException("Wrong message token kind") return func(kind, uaid, chid) @cyclone.web.asynchronous @@ -409,6 +410,7 @@ def put(self, api_ver="v0", token=None): Primary entry-point to handling a notification for a push client. """ + api_ver = api_ver or "v0" self.start_time = time.time() public_key = None keys = {} @@ -436,7 +438,7 @@ def put(self, api_ver="v0", token=None): def _token_valid(self, result): """Called after the token is decrypted successfully""" if len(result) != 2: - raise ValueError("Wrong subscription token components") + raise InvalidTokenException("Wrong subscription token components") self.uaid, self.chid = result d = deferToThread(self.ap_settings.router.get_uaid, self.uaid) diff --git a/autopush/exceptions.py b/autopush/exceptions.py index dc881f48..259445cf 100644 --- a/autopush/exceptions.py +++ b/autopush/exceptions.py @@ -3,3 +3,7 @@ class AutopushException(Exception): """Parent Autopush Exception""" + + +class InvalidTokenException(Exception): + """Invalid URL token Exception""" diff --git a/autopush/settings.py b/autopush/settings.py index 41af7fa1..1d06f465 100644 --- a/autopush/settings.py +++ b/autopush/settings.py @@ -24,6 +24,7 @@ Router, Message ) +from autopush.exceptions import InvalidTokenException from autopush.metrics import ( DatadogMetrics, TwistedMetrics, @@ -293,16 +294,16 @@ def parse_endpoint(self, token, version="v0", public_key=None): if version == 'v0': if ':' not in token: - raise ValueError("Corrupted push token") + raise InvalidTokenException("Corrupted push token") return tuple(token.split(':')) if version == 'v1' and len(token) != 32: - raise ValueError("Corrupted push token") + raise InvalidTokenException("Corrupted push token") if version == 'v2': if len(token) != 64: - raise ValueError("Corrupted push token") + raise InvalidTokenException("Corrupted push token") if not public_key: - raise ValueError("Invalid key data") + raise InvalidTokenException("Invalid key data") if not constant_time.bytes_eq(sha256(public_key).digest(), token[32:]): - raise ValueError("Key mismatch") + raise InvalidTokenException("Key mismatch") return (token[:16].encode('hex'), token[16:32].encode('hex')) diff --git a/autopush/tests/test_endpoint.py b/autopush/tests/test_endpoint.py index 4981d462..1f4f787a 100644 --- a/autopush/tests/test_endpoint.py +++ b/autopush/tests/test_endpoint.py @@ -21,6 +21,7 @@ from twisted.web.client import Agent, Response from txstatsd.metrics.metrics import Metrics + import autopush.endpoint as endpoint import autopush.utils as utils from autopush.db import ( @@ -33,6 +34,7 @@ has_connected_this_month, hasher ) +from autopush.exceptions import InvalidTokenException from autopush.settings import AutopushSettings from autopush.router.interface import IRouter, RouterResponse from autopush.senderids import SenderIDs @@ -41,6 +43,7 @@ mock_dynamodb2 = mock_dynamodb2() dummy_uaid = str(uuid.UUID("abad1dea00000000aabbccdd00000000")) dummy_chid = str(uuid.UUID("deadbeef00000000decafbad00000000")) +dummy_token = dummy_uaid + ":" + dummy_chid def setUp(): @@ -307,7 +310,7 @@ def handle_finish(value): def test_webpush_missing_ttl_user_offline(self): from autopush.router.interface import RouterException - self.fernet_mock.decrypt.return_value = "123:456" + self.fernet_mock.decrypt.return_value = dummy_token self.endpoint.set_header = Mock() del(self.request_mock.headers["ttl"]) self.request_mock.headers["encryption"] = "stuff" @@ -508,7 +511,7 @@ def test_load_params_prefer_body(self, t): eq_(data, 'bai') def test_put_data_too_large(self): - self.fernet_mock.decrypt.return_value = "123:456" + self.fernet_mock.decrypt.return_value = dummy_token self.endpoint.ap_settings.router.get_uaid.return_value = {} self.endpoint.ap_settings.max_data = 3 self.endpoint.request.body = b'version=1&data=1234' @@ -576,7 +579,7 @@ def handle_finish(result): self.endpoint.version, self.endpoint.data = 789, None - exc = self.assertRaises(ValueError, + exc = self.assertRaises(InvalidTokenException, self.endpoint._token_valid, ['invalid']) eq_(exc.message, "Wrong subscription token components") @@ -585,7 +588,7 @@ def handle_finish(result): return self.finish_deferred def test_put_default_router(self): - self.fernet_mock.decrypt.return_value = "123:456" + self.fernet_mock.decrypt.return_value = dummy_token self.router_mock.get_uaid.return_value = dict() self.sp_router_mock.route_notification.return_value = RouterResponse() @@ -602,7 +605,7 @@ def test_put_router_with_headers(self): self.request_mock.headers["content-encoding"] = 'text' self.request_mock.headers["encryption-key"] = "encKey" self.request_mock.body = b' ' - self.fernet_mock.decrypt.return_value = "123:456" + self.fernet_mock.decrypt.return_value = dummy_token self.router_mock.get_uaid.return_value = dict( router_type="webpush", router_data=dict(), @@ -621,7 +624,7 @@ def handle_finish(result): return self.finish_deferred def test_put_router_needs_change(self): - self.fernet_mock.decrypt.return_value = "123:456" + self.fernet_mock.decrypt.return_value = dummy_token self.router_mock.get_uaid.return_value = dict( router_type="simplepush", router_data=dict(), @@ -641,7 +644,7 @@ def handle_finish(result): return self.finish_deferred def test_put_router_needs_update(self): - self.fernet_mock.decrypt.return_value = "123:456" + self.fernet_mock.decrypt.return_value = dummy_token self.router_mock.get_uaid.return_value = dict( router_type="simplepush", router_data=dict(), @@ -666,7 +669,7 @@ def test_put_bogus_headers(self): self.request_mock.headers["encryption-key"] = "encKey" self.request_mock.headers["crypto-key"] = "fake=crypKey" self.request_mock.body = b' ' - self.fernet_mock.decrypt.return_value = "123:456" + self.fernet_mock.decrypt.return_value = dummy_token self.router_mock.get_uaid.return_value = dict( router_type="webpush", router_data=dict(), @@ -690,7 +693,7 @@ def test_put_invalid_vapid_crypto_header(self): self.request_mock.headers["authorization"] = "some auth" self.request_mock.headers["crypto-key"] = "crypKey" self.request_mock.body = b' ' - self.fernet_mock.decrypt.return_value = "123:456" + self.fernet_mock.decrypt.return_value = dummy_token self.router_mock.get_uaid.return_value = dict( router_type="webpush", router_data=dict(), @@ -714,7 +717,7 @@ def test_put_invalid_vapid_crypto_key(self): self.request_mock.headers["authorization"] = "invalid" self.request_mock.headers["crypto-key"] = "crypt=crap" self.request_mock.body = b' ' - self.fernet_mock.decrypt.return_value = "123:456" + self.fernet_mock.decrypt.return_value = dummy_token self.router_mock.get_uaid.return_value = dict( router_type="webpush", router_data=dict(), @@ -738,7 +741,7 @@ def test_put_invalid_vapid_auth_header(self): self.request_mock.headers["authorization"] = "invalid" self.request_mock.headers["crypto-key"] = "p256ecdsa=crap" self.request_mock.body = b' ' - self.fernet_mock.decrypt.return_value = "123:456" + self.fernet_mock.decrypt.return_value = dummy_token self.router_mock.get_uaid.return_value = dict( router_type="webpush", router_data=dict(), @@ -761,7 +764,7 @@ def test_put_missing_vapid_crypto_header(self): self.request_mock.headers["content-encoding"] = 'text' self.request_mock.headers["authorization"] = "some auth" self.request_mock.body = b' ' - self.fernet_mock.decrypt.return_value = "123:456" + self.fernet_mock.decrypt.return_value = dummy_token self.router_mock.get_uaid.return_value = dict( router_type="webpush", router_data=dict(), @@ -780,7 +783,7 @@ def handle_finish(result): return self.finish_deferred def test_post_webpush_with_headers_in_response(self): - self.fernet_mock.decrypt.return_value = "123:456" + self.fernet_mock.decrypt.return_value = dummy_token self.endpoint.set_header = Mock() self.request_mock.headers["encryption"] = "stuff" self.request_mock.headers["content-encoding"] = "aes128" @@ -811,7 +814,7 @@ def _gen_jwt(self, header, payload): return (sig, crypto_key) def test_post_webpush_with_vapid_auth(self): - self.fernet_mock.decrypt.return_value = "123:456" + self.fernet_mock.decrypt.return_value = dummy_token self.endpoint.set_header = Mock() self.request_mock.headers["encryption"] = "stuff" self.request_mock.headers["content-encoding"] = "aes128" @@ -873,7 +876,7 @@ def test_decipher_public_key(self): self.assertRaises(ValueError, decipher_public_key, crap[:60]) def test_post_webpush_with_other_than_vapid_auth(self): - self.fernet_mock.decrypt.return_value = "123:456" + self.fernet_mock.decrypt.return_value = dummy_token self.endpoint.set_header = Mock() self.request_mock.headers["encryption"] = "stuff" self.request_mock.headers["content-encoding"] = "aes128" @@ -905,7 +908,7 @@ def handle_finish(result): return self.finish_deferred def test_post_webpush_with_bad_vapid_auth(self): - self.fernet_mock.decrypt.return_value = "123:456" + self.fernet_mock.decrypt.return_value = dummy_token self.endpoint.set_header = Mock() self.request_mock.headers["encryption"] = "stuff" self.request_mock.headers["content-encoding"] = "aes128" @@ -937,7 +940,7 @@ def handle_finish(result): return self.finish_deferred def test_post_webpush_no_sig(self): - self.fernet_mock.decrypt.return_value = "123:456" + self.fernet_mock.decrypt.return_value = dummy_token self.endpoint.set_header = Mock() self.request_mock.headers["encryption"] = "stuff" self.request_mock.headers["content-encoding"] = "aes128" @@ -983,7 +986,7 @@ def test_util_extract_jwt(self): eq_(utils.extract_jwt(sig, crypto_key), payload) def test_post_webpush_bad_sig(self): - self.fernet_mock.decrypt.return_value = "123:456" + self.fernet_mock.decrypt.return_value = dummy_token self.endpoint.set_header = Mock() self.request_mock.headers["encryption"] = "stuff" self.request_mock.headers["content-encoding"] = "aes128" @@ -1017,7 +1020,7 @@ def handle_finish(result): return self.finish_deferred def test_post_webpush_bad_exp(self): - self.fernet_mock.decrypt.return_value = "123:456" + self.fernet_mock.decrypt.return_value = dummy_token self.endpoint.set_header = Mock() self.request_mock.headers["encryption"] = "stuff" self.request_mock.headers["content-encoding"] = "aes128" @@ -1049,7 +1052,7 @@ def handle_finish(result): return self.finish_deferred def test_post_webpush_with_auth(self): - self.fernet_mock.decrypt.return_value = "123:456" + self.fernet_mock.decrypt.return_value = dummy_token self.endpoint.set_header = Mock() self.request_mock.headers["encryption"] = "stuff" self.request_mock.headers["content-encoding"] = "aes128" @@ -1077,7 +1080,7 @@ def test_post_webpush_with_logged_delivered(self): import autopush.endpoint log_patcher = patch.object(autopush.endpoint.log, "msg", spec=True) mock_log = log_patcher.start() - self.fernet_mock.decrypt.return_value = "123:456" + self.fernet_mock.decrypt.return_value = dummy_token self.endpoint.set_header = Mock() self.request_mock.headers["encryption"] = "stuff" self.request_mock.headers["content-encoding"] = "aes128" @@ -1108,7 +1111,7 @@ def test_post_webpush_with_logged_stored(self): import autopush.endpoint log_patcher = patch.object(autopush.endpoint.log, "msg", spec=True) mock_log = log_patcher.start() - self.fernet_mock.decrypt.return_value = "123:456" + self.fernet_mock.decrypt.return_value = dummy_token self.endpoint.set_header = Mock() self.request_mock.headers["encryption"] = "stuff" self.request_mock.headers["content-encoding"] = "aes128" @@ -1138,7 +1141,7 @@ def handle_finish(result): @patch("twisted.python.log") def test_post_db_error_in_routing(self, mock_log): from autopush.router.interface import RouterException - self.fernet_mock.decrypt.return_value = "123:456" + self.fernet_mock.decrypt.return_value = dummy_token self.endpoint.set_header = Mock() self.request_mock.headers["encryption"] = "stuff" self.request_mock.headers["content-encoding"] = "aes128" @@ -1167,7 +1170,7 @@ def handle_finish(result): return self.finish_deferred def test_put_db_error(self): - self.fernet_mock.decrypt.return_value = "123:456" + self.fernet_mock.decrypt.return_value = dummy_token self.router_mock.get_uaid.side_effect = self._throw_provisioned_error def handle_finish(result): @@ -1281,13 +1284,13 @@ def test_parse_endpoint(self): # v0 bad self.fernet_mock.decrypt.return_value = v1_valid - exc = self.assertRaises(ValueError, + exc = self.assertRaises(InvalidTokenException, self.settings.parse_endpoint, '/invalid') eq_(exc.message, 'Corrupted push token') self.fernet_mock.decrypt.return_value = v1_valid[:30] - exc = self.assertRaises(ValueError, + exc = self.assertRaises(InvalidTokenException, self.settings.parse_endpoint, 'invalid', 'v1') eq_(exc.message, 'Corrupted push token') @@ -1301,25 +1304,25 @@ def test_parse_endpoint(self): eq_(tokens, (uaid_strip, chid_strip)) self.fernet_mock.decrypt.return_value = v1_valid + "invalid" - exc = self.assertRaises(ValueError, + exc = self.assertRaises(InvalidTokenException, self.settings.parse_endpoint, 'invalid', 'v2', pub_key) eq_(exc.message, "Corrupted push token") self.fernet_mock.decrypt.return_value = v1_valid + v2_valid - exc = self.assertRaises(ValueError, + exc = self.assertRaises(InvalidTokenException, self.settings.parse_endpoint, 'invalid', 'v2', pub_key[:30]) eq_(exc.message, "Key mismatch") self.fernet_mock.decrypt.return_value = v1_valid + v2_invalid - exc = self.assertRaises(ValueError, + exc = self.assertRaises(InvalidTokenException, self.settings.parse_endpoint, 'invalid', 'v2') eq_(exc.message, "Invalid key data") self.fernet_mock.decrypt.return_value = v1_valid + v2_invalid - exc = self.assertRaises(ValueError, + exc = self.assertRaises(InvalidTokenException, self.settings.parse_endpoint, 'invalid', 'v2', pub_key) eq_(exc.message, "Key mismatch") diff --git a/autopush/tests/test_integration.py b/autopush/tests/test_integration.py index b6ebbc5f..63193fb8 100644 --- a/autopush/tests/test_integration.py +++ b/autopush/tests/test_integration.py @@ -188,11 +188,11 @@ def delete_notification(self, channel, message=None, status=204): def send_notification(self, channel=None, version=None, data=None, use_header=True, status=None, ttl=200, - timeout=0.2, vapid=None): + timeout=0.2, vapid=None, endpoint=None): if not channel: channel = random.choice(self.channels.keys()) - endpoint = self.channels[channel] + endpoint = endpoint or self.channels[channel] url = urlparse.urlparse(endpoint) http = None if url.scheme == "https": # pragma: nocover @@ -376,6 +376,11 @@ def setUp(self): self.website = reactor.listenTCP(9020, site) self._settings = settings + def _make_v0_endpoint(self, uaid, chid): + return self._settings.endpoint_url + '/push/' + \ + self._settings.fernet.encrypt( + (uaid + ":" + chid).encode('utf-8')) + @inlineCallbacks def tearDown(self): dones = [self.websocket.stopListening(), self.website.stopListening(), @@ -689,6 +694,18 @@ def test_basic_delivery(self): eq_(result["messageType"], "notification") yield self.shut_down(client) + @inlineCallbacks + def test_basic_delivery_v0_endpoint(self): + data = str(uuid.uuid4()) + client = yield self.quick_register(use_webpush=True) + endpoint = self._make_v0_endpoint( + client.uaid, client.channels.keys()[0]) + result = yield client.send_notification(endpoint=endpoint, data=data) + eq_(result["headers"]["encryption"], client._crypto_key) + eq_(result["data"], urlsafe_b64encode(data)) + eq_(result["messageType"], "notification") + yield self.shut_down(client) + @inlineCallbacks def test_basic_delivery_with_vapid(self): data = str(uuid.uuid4())