From 71035fa0e95825c1058d1c3d8debade519c50c2a Mon Sep 17 00:00:00 2001 From: jrconlin Date: Thu, 16 Mar 2017 11:46:09 -0700 Subject: [PATCH] feat: Add ChannelID report for UAID Adds new bridge system HTTP endpoint to fetch a list of server known CHIDs for a given UAID. Useful for remote clients to check status. (see documentation for calling structure and return). Also fixed an edge case where clients that may have been forced offline due to external system errors could still register new endpoints. The behavior will now be to return a 410 error if a client has been flagged as disconnected by the server. It is up to the client to check the local bridge connection and re-establish any endpoints. closes: #844, #843 --- autopush/db.py | 3 ++- autopush/tests/test_endpoint.py | 47 ++++++++++++++++++++++++++++++++- autopush/web/registration.py | 40 ++++++++++++++++++++++++++-- docs/http.rst | 41 ++++++++++++++++++++++++++++ 4 files changed, 127 insertions(+), 4 deletions(-) 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`.