diff --git a/autopush/endpoint.py b/autopush/endpoint.py index 89e38065..5522128b 100644 --- a/autopush/endpoint.py +++ b/autopush/endpoint.py @@ -52,6 +52,8 @@ extract_jwt, base64url_encode, ) +from autopush.web.base import DEFAULT_ERR_URL + # Our max TTL is 60 days realistically with table rotation, so we hard-code it MAX_TTL = 60 * 60 * 24 * 60 @@ -193,16 +195,18 @@ def _init_info(self): # Error Callbacks ############################################################# def _write_response(self, status_code, errno, message=None, headers=None, - reason=None): + reason=None, url=None): """Writes out a full JSON error and sets the appropriate status""" self.set_status(status_code, reason) error_data = dict( code=status_code, errno=errno, - error=status_codes.get(status_code, reason or "") + error=status_codes.get(status_code, reason or ""), ) if message: error_data["message"] = message + if status_code > 299 and url is None: + error_data["more_info"] = DEFAULT_ERR_URL self.write(json.dumps(error_data)) self.set_header("Content-Type", "application/json") if headers: @@ -210,9 +214,9 @@ def _write_response(self, status_code, errno, message=None, headers=None, self.set_header(header, headers.get(header)) self.finish() - def _write_unauthorized_response(self, message=None): + def _write_unauthorized_response(self, message="Invalid authentication"): headers = {"www-authenticate": AUTH_SCHEME} - self._write_response(401, 109, message, headers) + self._write_response(401, errno=109, message=message, headers=headers) def _response_err(self, fail): """errBack for all exceptions that should be logged @@ -224,14 +228,16 @@ def _response_err(self, fail): fmt = fail.value.message or 'Exception' self.log.failure(format=fmt, failure=fail, status_code=500, errno=999, **self._client_info) - self._write_response(500, 999) + self._write_response(500, errno=999, + message="Unexpected server error occurred") def _overload_err(self, fail): """errBack for throughput provisioned exceptions""" fail.trap(ProvisionedThroughputExceededException) self.log.info(format="Throughput Exceeded", status_code=503, errno=201, **self._client_info) - self._write_response(503, 201) + self._write_response(503, errno=201, + message="Please slow message send rate") def _router_response(self, response): for name, val in response.headers.items(): @@ -276,7 +282,9 @@ def _uaid_not_found_err(self, fail): self.log.info(format="UAID not found in AWS.", status_code=410, errno=103, **self._client_info) - self._write_response(410, 103) + self._write_response(410, errno=103, + message="Endpoint has expired. " + "Do not send messages to this endpoint.") def _token_err(self, fail): """errBack for token decryption fail""" @@ -284,7 +292,8 @@ def _token_err(self, fail): self.log.info(format="Invalid token", status_code=400, errno=102, **self._client_info) - self._write_response(400, 102) + self._write_response(400, 102, + message="Invalid endpoint.") def _auth_err(self, fail): """errBack for invalid auth token""" @@ -301,7 +310,7 @@ def _chid_not_found_err(self, fail): self.log.info(format="CHID not found in AWS.", status_code=410, errno=106, **self._client_info) - self._write_response(410, 106) + self._write_response(410, 106, message="Invalid endpoint.") ############################################################# # Utility Methods @@ -441,7 +450,7 @@ def put(self, api_ver="v0", token=None): "web-push-encryption") self._write_response( 400, - 110, + errno=110, message="You're using outdated encryption; " "Please update to the format described in " + wpe_url) return @@ -500,7 +509,8 @@ def _uaid_lookup_results(self, result): for x in req_fields]): self.log.debug(format="Client error", status_code=400, errno=101, **self._client_info) - return self._write_response(400, 101) + return self._write_response( + 400, errno=101, message="Missing necessary crypto keys.") if ("encryption-key" in self.request.headers and "crypto-key" in self.request.headers): self.log.debug(format="Client error", status_code=400, diff --git a/autopush/router/webpush.py b/autopush/router/webpush.py index 7c523fb5..6c88570a 100644 --- a/autopush/router/webpush.py +++ b/autopush/router/webpush.py @@ -66,11 +66,15 @@ def preflight_check(self, uaid, channel_id): record = yield deferToThread(self.ap_settings.router.get_uaid, uaid) if 'current_month' not in record: + self.log.info("Record missing 'current_month' {record}", + record=json.dumps(record)) raise RouterException("No such subscription", status_code=410, log_exception=False, errno=106) month_table = record["current_month"] if month_table not in self.ap_settings.message_tables: + self.log.info("'current_month' out of scope: {record}", + records=json.dumps(record)) yield deferToThread(self.ap_settings.router.drop_user, uaid) raise RouterException("No such subscription", status_code=410, log_exception=False, errno=106) @@ -80,6 +84,8 @@ def preflight_check(self, uaid, channel_id): if (not exists or channel_id.lower() not in map(lambda x: normalize_id(x), chans)): + self.log.info("Unknown subscription: {channel_id}", + channelid=channel_id) raise RouterException("No such subscription", status_code=410, log_exception=False, errno=106) returnValue(month_table) diff --git a/autopush/tests/test_endpoint.py b/autopush/tests/test_endpoint.py index 65a07171..fc236ed7 100644 --- a/autopush/tests/test_endpoint.py +++ b/autopush/tests/test_endpoint.py @@ -1701,7 +1701,7 @@ def mock_encrypt(cleartext): # sha256(dummy_key).digest() eq_(cleartext[32:], ('47aedd050b9e19171f0fa7b8b65ca670' - '28f0bc92cd3f2cd3682b1200ec759007').decode('hex')) + '28f0bc92cd3f2cd3682b1200ec759007').decode('hex')) return 'abcd123' self.fernet_mock.configure_mock(**{ 'encrypt.side_effect': mock_encrypt, diff --git a/autopush/web/base.py b/autopush/web/base.py index 1d6799c3..ccb5251d 100644 --- a/autopush/web/base.py +++ b/autopush/web/base.py @@ -29,6 +29,9 @@ 503: "Service Unavailable", } +DEFAULT_ERR_URL = ("http://autopush.readthedocs.io/en/latest/http.html" + "#error-codes") + class Notification(namedtuple("Notification", "version data channel_id headers ttl")): @@ -141,13 +144,15 @@ def _init_info(self): ############################################################# # Error Callbacks ############################################################# - def _write_response(self, status_code, errno, message=None, headers=None): + def _write_response(self, status_code, errno, message=None, headers=None, + url=DEFAULT_ERR_URL): """Writes out a full JSON error and sets the appropriate status""" self.set_status(status_code) error_data = dict( code=status_code, errno=errno, - error=status_codes.get(status_code, "") + error=status_codes.get(status_code, ""), + more_info=url, ) if message: error_data["message"] = message @@ -165,7 +170,9 @@ def _validation_err(self, fail): self.log.info(format="Request validation error", status_code=exc.status_code, errno=exc.errno) - self._write_response(exc.status_code, exc.errno, headers=exc.headers) + self._write_response(exc.status_code, exc.errno, + message="Request did not validate", + headers=exc.headers) def _response_err(self, fail): """errBack for all exceptions that should be logged @@ -177,14 +184,16 @@ def _response_err(self, fail): fmt = fail.value.message or 'Exception' self.log.failure(format=fmt, failure=fail, status_code=500, errno=999, **self._client_info) - self._write_response(500, 999) + self._write_response(500, 999, message="An unexpected server error" + " occurred.") def _overload_err(self, fail): """errBack for throughput provisioned exceptions""" fail.trap(ProvisionedThroughputExceededException) self.log.info(format="Throughput Exceeded", status_code=503, errno=201, **self._client_info) - self._write_response(503, 201) + self._write_response(503, 201, + message="Please slow message send rate") def _router_response(self, response): for name, val in response.headers.items(): diff --git a/autopush/web/validation.py b/autopush/web/validation.py index 19d340b0..71dd8779 100644 --- a/autopush/web/validation.py +++ b/autopush/web/validation.py @@ -206,7 +206,7 @@ def validate_uaid(self, d): except ItemNotFound: raise InvalidRequest("UAID not found", status_code=410, errno=103) - if result.get("router_type") not in ["webpush", "gcm", "apns"]: + if result.get("router_type") not in ["webpush", "gcm", "apns", "fcm"]: raise InvalidRequest("Wrong URL for user", errno=108) # Propagate the looked up user data back out diff --git a/autopush/websocket.py b/autopush/websocket.py index adfec15c..d0fff37e 100644 --- a/autopush/websocket.py +++ b/autopush/websocket.py @@ -73,6 +73,8 @@ USER_RECORD_VERSION = 1 +DEFAULT_WS_ERR = "http://autopush.readthedocs.io/en/" \ + "latest/api/websocket.html#private-http-endpoint" def extract_code(data): @@ -555,12 +557,16 @@ def _notify_node(self, result): ).addCallback(IgnoreBody.ignore) d.addErrback(self.log_failure, extra="Failed to notify node") - def returnError(self, messageType, reason, statusCode, close=True): + def returnError(self, messageType, reason, statusCode, close=True, + message="", url=DEFAULT_WS_ERR): """Return an error to a client, and optionally shut down the connection safely""" - self.sendJSON({"messageType": messageType, - "reason": reason, - "status": statusCode}) + send = {"messageType": messageType, + "reason": reason, + "status": statusCode} + if url: + send["more_info"] = url + self.sendJSON(send) if close: self.sendClose() @@ -723,7 +729,7 @@ def err_hello(self, failure): self.log_failure(failure) self.returnError("hello", "error", 503) - def _check_other_nodes(self, result): + def _check_other_nodes(self, result, url=DEFAULT_WS_ERR): """callback to check other nodes for clients and send them a delete as needed""" self.transport.resumeProducing() @@ -732,7 +738,8 @@ def _check_other_nodes(self, result): if not registered: # Registration failed msg = {"messageType": "hello", "reason": "already_connected", - "status": 500} + "status": 500, + "more_info": url} self.sendJSON(msg) return @@ -1008,7 +1015,8 @@ def process_register(self, data): def error_register(self, fail): """errBack handler for registering to fail""" self.transport.resumeProducing() - msg = {"messageType": "register", "status": 500} + msg = {"messageType": "register", "status": 500, + "reason": "An unexpected server error occurred"} self.sendJSON(msg) def finish_register(self, endpoint, chid): @@ -1223,11 +1231,11 @@ def check_missed_notifications(self, results, resume=False): if self.ps._check_notifications or self.ps._more_notifications: self.process_notifications() - def bad_message(self, typ, message=None): + def bad_message(self, typ, message=None, url=DEFAULT_WS_ERR): """Error helper for sending a 401 status back""" - msg = {"messageType": typ, "status": 401} + msg = {"messageType": typ, "status": 401, "more_info": url} if message: - msg["message"] = message + msg["reason"] = message self.sendJSON(msg) def _newer_notification_sent(self, channel_id, version): @@ -1293,6 +1301,7 @@ def put(self, uaid): if client.paused: self.set_status(503) + settings.metrics.increment("updates.router.busy") return self.write("Client busy.")