diff --git a/autopush/db.py b/autopush/db.py index 9c7799e7..027b6261 100644 --- a/autopush/db.py +++ b/autopush/db.py @@ -495,7 +495,8 @@ def all_channels(self, uaid): # Functions that call store_message() would be required to # update that list as well using register_channel() try: - result = self.table.get_item(consistent=True, uaid=hasher(uaid), + result = self.table.get_item(consistent=True, + uaid=hasher(uaid), chidmessageid=" ") return (True, result["chids"] or set([])) except ItemNotFound: diff --git a/autopush/tests/test_endpoint.py b/autopush/tests/test_endpoint.py index d30cedc6..d70f972b 100644 --- a/autopush/tests/test_endpoint.py +++ b/autopush/tests/test_endpoint.py @@ -207,7 +207,7 @@ def handle_finish(result): return self.finish_deferred -CORS_HEAD = "POST,PUT,DELETE" +CORS_HEAD = "GET,POST,PUT,DELETE" class RegistrationTestCase(unittest.TestCase): @@ -591,6 +591,31 @@ def restore(*args, **kwargs): chid=str(dummy_chid))) return self.finish_deferred + def test_post_uaid_critical_failure(self, *args): + self.reg.request.body = json.dumps(dict( + type="webpush", + channelID=str(dummy_chid), + data={}, + )) + self.settings.router.get_uaid = Mock() + self.settings.router.get_uaid.return_value = { + "critical_failure": "Client is unreachable due to a configuration " + "error." + } + self.fernet_mock.configure_mock(**{ + 'encrypt.return_value': 'abcd123', + }) + + def handle_finish(value): + self._check_error(410, 105, "") + + self.finish_deferred.addCallback(handle_finish) + self.reg.request.headers["Authorization"] = self.auth + self.reg.post(self._make_req(router_type="simplepush", + uaid=dummy_uaid.hex, + chid=str(dummy_chid))) + return self.finish_deferred + def test_post_nochid(self): self.reg.request.body = json.dumps(dict( type="simplepush", @@ -834,3 +859,23 @@ def handle_finish(value): self.finish_deferred.addCallback(handle_finish) self.reg.delete(self._make_req("invalid", "test", dummy_uaid.hex)) return self.finish_deferred + + def test_get(self): + self.reg.request.headers['Authorization'] = self.auth + chids = [str(dummy_chid), str(dummy_uaid)] + + def handle_finish(value): + call_args = json.loads( + self.reg.write.call_args[0][0] + ) + eq_(chids, call_args['channelIDs']) + eq_(dummy_uaid.hex, call_args['uaid']) + + self.finish_deferred.addCallback(handle_finish) + self.settings.message.all_channels = Mock() + self.settings.message.all_channels.return_value = (True, chids) + self.reg.get(self._make_req( + router_type="test", + router_token="test", + uaid=dummy_uaid.hex)) + return self.finish_deferred diff --git a/autopush/web/registration.py b/autopush/web/registration.py index 4f59540a..c1033fa5 100644 --- a/autopush/web/registration.py +++ b/autopush/web/registration.py @@ -51,16 +51,27 @@ def extract_data(self, req): chid = req['path_kwargs'].get('chid', params.get("channelID")) if uaid: try: - uuid.UUID(uaid) + u_uuid = uuid.UUID(uaid) except (ValueError, TypeError): raise InvalidRequest("Invalid Request UAID", status_code=401, errno=109) + # Check if the UAID has a 'critical error' which means that it's + # probably invalid and should be reset/re-registered + try: + record = self.context['settings'].router.get_uaid(u_uuid.hex) + if record.get('critical_failure'): + raise InvalidRequest("Invalid Request UAID", + status_code=410, errno=105) + except ItemNotFound: + pass + if chid: try: uuid.UUID(chid) except (ValueError, TypeError): raise InvalidRequest("Invalid Request Channel_id", status_code=410, errno=106) + return dict( auth=req.get('headers', {}).get("Authorization"), params=params, @@ -121,7 +132,7 @@ def validate_data(self, data): class RegistrationHandler(BaseWebHandler): """Handle the Bridge services endpoints""" - cors_methods = "POST,PUT,DELETE" + cors_methods = "GET,POST,PUT,DELETE" ############################################################# # Cyclone HTTP Methods @@ -215,6 +226,22 @@ def _register_channel(self, router_data=None): self.app_server_key) return endpoint, router_data + @threaded_validate(RegistrationSchema) + def get(self, *args, **kwargs): + """HTTP GET + + Return a list of known channelIDs for a given UAID + + """ + self.uaid = self.valid_input['uaid'] + self.add_header("Content-Type", "application/json") + d = deferToThread(self.ap_settings.message.all_channels, + str(self.uaid)) + d.addCallback(self._write_channels) + d.addErrback(self._response_err) + d.addErrback(self._uaid_not_found_err) + return d + @threaded_validate(RegistrationSchema) def delete(self, *args, **kwargs): """HTTP DELETE @@ -307,6 +334,15 @@ def _return_endpoint(self, endpoint_data, new_uaid, router=None): client_info=self._client_info) self.finish() + def _write_channels(self, channel_info, *args, **kwargs): + # channel_info is a tuple containing a flag and the list of channels + dashed = [str(uuid.UUID(x)) for x in channel_info[1]] + self.write(json.dumps( + {"uaid": self.uaid.hex, + "channelIDs": dashed} + )) + self.finish() + def _success(self, result): """Writes out empty 200 response""" self.write({}) diff --git a/docs/http.rst b/docs/http.rst index e3ac8c68..395ee7e5 100644 --- a/docs/http.rst +++ b/docs/http.rst @@ -509,4 +509,45 @@ Remove a given ChannelID subscription from a UAID. **Return Codes:** +See :ref:`errors`. + +Get Known Channels for a UAID +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Fetch the known ChannelIDs for a given bridged endpoint. This is useful to check link status. +If no channelIDs are present for a given UAID, an empty set of channelIDs will be returned. + +**Call:** + +.. http:get:: /v1/{type}/{app_id}/registration/{UAID}/ + + Authorization: Bearer {secret} + +**Parameters:** + + {} + +**Reply:** + +.. code-block:: json + + {"uaid": {UAID}, "channelIDs": [{ChannelID}, ...]} + +example: + +.. code-block:: http + + > GET /v1/gcm/33clienttoken33/registration/abcdef012345/ + > Authorization: Bearer 00secret00 + > + > {} + +.. code-block:: json + + < {"uaid": "abcdef012345", + < "channelIDS": ["01234567-0000-1111-2222-0123456789ab", "76543210-0000-1111-2222-0123456789ab"]} + +**Return Codes:** + + See :ref:`errors`.