def test_ttl_batch_partly_expired_and_good_one(self): data = str(uuid.uuid4()) data1 = str(uuid.uuid4()) data2 = str(uuid.uuid4()) client = yield self.quick_register() yield client.disconnect() for x in range(0, 6): yield client.send_notification(data=data, status=201) for x in range(0, 6): yield client.send_notification(data=data1, ttl=1, status=201) yield client.send_notification(data=data2, status=201) time.sleep(1) yield client.connect() yield client.hello() # Pull out and ack the first for x in range(0, 6): result = yield client.get_notification(timeout=4) assert result is not None assert result["data"] == base64url_encode(data) yield client.ack(result["channelID"], result["version"]) # Should have one more that is data2, this will only arrive if the # other six were acked as that hits the batch size result = yield client.get_notification(timeout=4) assert result is not None assert result["data"] == base64url_encode(data2) # No more result = yield client.get_notification() assert result is None yield self.shut_down(client)
def test_repeat_delivery_with_disconnect_without_ack(self): data = str(uuid.uuid4()) client = yield self.quick_register() result = yield client.send_notification(data=data) assert result != {} assert result["data"] == base64url_encode(data) yield client.disconnect() yield client.connect() yield client.hello() result = yield client.get_notification() assert result != {} assert result["data"] == base64url_encode(data) yield self.shut_down(client)
def test_post_with_app_server_key(self, *args): self.patch('uuid.uuid4', return_value=dummy_chid) dummy_key = "RandomKeyString" def mock_encrypt(cleartext): assert len(cleartext) == 64 # dummy_uaid assert cleartext[0:16] == ( 'abad1dea00000000aabbccdd00000000'.decode('hex')) # dummy_chid assert cleartext[16:32] == ( 'deadbeef00000000decafbad00000000'.decode('hex')) # sha256(dummy_key).digest() assert cleartext[32:] == ( '47aedd050b9e19171f0fa7b8b65ca670' '28f0bc92cd3f2cd3682b1200ec759007').decode('hex') return 'abcd123' self.fernet_mock.configure_mock(**{ 'encrypt.side_effect': mock_encrypt, }) resp = yield self.client.post( self.url(router_type="webpush", uaid=dummy_uaid.hex) + "/subscription", headers={"Authorization": self.auth}, body=json.dumps(dict( type="webpush", key=utils.base64url_encode(dummy_key), data={}, )) ) payload = json.loads(resp.content) assert payload["channelID"] == dummy_chid.hex assert payload["endpoint"] == "http://localhost/wpush/v2/abcd123"
def fixup_output(self, d): # Verify authorization # Note: This has to be done here, since schema validation takes place # before nested schemas, and in this case we need all the nested # schema logic to run first. self.validate_auth(d) # Merge crypto headers back in if d["crypto_headers"]: d["headers"].update({ k.replace("_", "-"): v for k, v in d["crypto_headers"].items() }) # Base64-encode data for Web Push d["body"] = base64url_encode(d["body"]) # Set the notification based on the validated request schema data d["notification"] = WebPushNotification.from_webpush_request_schema( data=d, fernet=self.context["settings"].fernet, legacy=self.context["settings"]._notification_legacy, ) return d
def test_basic_delivery(self): data = str(uuid.uuid4()) client = yield self.quick_register(use_webpush=True) result = yield client.send_notification(data=data) eq_(result["headers"]["encryption"], client._crypto_key) eq_(result["data"], base64url_encode(data)) eq_(result["messageType"], "notification") yield self.shut_down(client)
def _store_auth(self, jwt, crypto_key, token, result): if jwt.get('exp', 0) < time.time(): raise VapidAuthException("Invalid bearer token: Auth expired") jwt_crypto_key = base64url_encode(crypto_key) self._client_info["jwt_crypto_key"] = jwt_crypto_key for i in jwt: self._client_info["jwt_" + i] = jwt[i] return result
def test_ttl_0_connected(self): data = str(uuid.uuid4()) client = yield self.quick_register(use_webpush=True) result = yield client.send_notification(data=data, ttl=0) assert(result is not None) eq_(result["headers"]["encryption"], client._crypto_key) eq_(result["data"], base64url_encode(data)) eq_(result["messageType"], "notification") yield self.shut_down(client)
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"], base64url_encode(data)) eq_(result["messageType"], "notification") yield self.shut_down(client)
def test_delivery_repeat_without_ack(self): data = str(uuid.uuid4()) client = yield self.quick_register(use_webpush=True) yield client.disconnect() ok_(client.channels) yield client.send_notification(data=data) yield client.connect() yield client.hello() result = yield client.get_notification() ok_(result != {}) eq_(result["data"], base64url_encode(data)) yield client.disconnect() yield client.connect() yield client.hello() result = yield client.get_notification() ok_(result != {}) eq_(result["data"], base64url_encode(data)) yield self.shut_down(client)
def test_topic_basic_delivery(self): data = str(uuid.uuid4()) client = yield self.quick_register() result = yield client.send_notification(data=data, topic="Inbox") # the following presumes that only `salt` is padded. clean_header = client._crypto_key.replace( '"', '').rstrip('=') assert result["headers"]["encryption"] == clean_header assert result["data"] == base64url_encode(data) assert result["messageType"] == "notification" yield self.shut_down(client)
def validate_auth(self, d): auth = d["headers"].get("authorization") needs_auth = d["token_info"]["api_ver"] == "v2" if not needs_auth and not auth: return try: vapid_auth = parse_auth_header(auth) token = vapid_auth['t'] d["vapid_version"] = "draft{:0>2}".format(vapid_auth['version']) if vapid_auth['version'] == 2: public_key = vapid_auth['k'] else: public_key = d["subscription"].get("public_key") jwt = extract_jwt( token, public_key, is_trusted=self.context['settings'].enable_tls_auth) except (KeyError, ValueError, InvalidSignature, TypeError, VapidAuthException): raise InvalidRequest("Invalid Authorization Header", status_code=401, errno=109, headers={"www-authenticate": PREF_SCHEME}) if "exp" not in jwt: raise InvalidRequest("Invalid bearer token: No expiration", status_code=401, errno=109, headers={"www-authenticate": PREF_SCHEME}) try: jwt_expires = int(jwt['exp']) except ValueError: raise InvalidRequest("Invalid bearer token: Invalid expiration", status_code=401, errno=109, headers={"www-authenticate": PREF_SCHEME}) now = time.time() jwt_has_expired = now > jwt_expires if jwt_has_expired: raise InvalidRequest("Invalid bearer token: Auth expired", status_code=401, errno=109, headers={"www-authenticate": PREF_SCHEME}) jwt_too_far_in_future = (jwt_expires - now) > (60 * 60 * 24) if jwt_too_far_in_future: raise InvalidRequest( "Invalid bearer token: Auth > 24 hours in " "the future", status_code=401, errno=109, headers={"www-authenticate": PREF_SCHEME}) jwt_crypto_key = base64url_encode(public_key) d["jwt"] = dict(jwt_crypto_key=jwt_crypto_key, jwt_data=jwt)
def test_basic_delivery_with_vapid(self): data = str(uuid.uuid4()) client = yield self.quick_register() vapid_info = _get_vapid( payload=self.vapid_payload) result = yield client.send_notification(data=data, vapid=vapid_info) # the following presumes that only `salt` is padded. clean_header = client._crypto_key.replace( '"', '').rstrip('=') assert result["headers"]["encryption"] == clean_header assert result["data"] == base64url_encode(data) assert result["messageType"] == "notification" yield self.shut_down(client)
def _get_vapid(key=None, payload=None): if not payload: payload = {"aud": "https://pusher_origin.example.com", "exp": int(time.time()) + 86400, "sub": "mailto:[email protected]"} if not key: key = ecdsa.SigningKey.generate(curve=ecdsa.NIST256p) vk = key.get_verifying_key() auth = jws.sign(payload, key, algorithm="ES256").strip('=') crypto_key = base64url_encode('\4' + vk.to_string()) return {"auth": auth, "crypto-key": crypto_key, "key": key}
def test_multiple_delivery_with_single_ack(self): data = str(uuid.uuid4()) data2 = str(uuid.uuid4()) client = yield self.quick_register() yield client.disconnect() assert client.channels yield client.send_notification(data=data, status=201) yield client.send_notification(data=data2, status=201) yield client.connect() yield client.hello() result = yield client.get_notification(timeout=0.5) assert result != {} assert result["data"] == base64url_encode(data) result2 = yield client.get_notification(timeout=0.5) assert result2 != {} assert result2["data"] == base64url_encode(data2) yield client.ack(result["channelID"], result["version"]) yield client.disconnect() yield client.connect() yield client.hello() result = yield client.get_notification(timeout=0.5) assert result != {} assert result["data"] == base64url_encode(data) assert result["messageType"] == "notification" result2 = yield client.get_notification() assert result2 != {} assert result2["data"] == base64url_encode(data2) yield client.ack(result["channelID"], result["version"]) yield client.ack(result2["channelID"], result2["version"]) # Verify no messages are delivered yield client.disconnect() yield client.connect() yield client.hello() result = yield client.get_notification(timeout=0.5) assert result is None yield self.shut_down(client)
def test_topic_expired(self): data = str(uuid.uuid4()) client = yield self.quick_register() yield client.disconnect() assert client.channels yield client.send_notification(data=data, ttl=1, topic="test", status=201) yield client.sleep(2) yield client.connect() yield client.hello() result = yield client.get_notification(timeout=0.5) assert result is None result = yield client.send_notification(data=data, topic="test") assert result != {} assert result["data"] == base64url_encode(data) yield self.shut_down(client)
def test_no_delivery_to_unregistered(self): data = str(uuid.uuid4()) client = yield self.quick_register(use_webpush=True) ok_(client.channels) chan = client.channels.keys()[0] result = yield client.send_notification(data=data) eq_(result["channelID"], chan) eq_(result["data"], base64url_encode(data)) yield client.ack(result["channelID"], result["version"]) yield client.unregister(chan) result = yield client.send_notification(data=data, status=404) eq_(result, None) yield self.shut_down(client)
def test_post_with_app_server_key(self): dummy_key = "RandomKeyString" self.reg.request.body = json.dumps(dict( type="simplepush", key=utils.base64url_encode(dummy_key), data={}, )) def mock_encrypt(cleartext): eq_(len(cleartext), 64) # dummy_uaid eq_(cleartext[0:16], 'abad1dea00000000aabbccdd00000000'.decode('hex')) # dummy_chid eq_(cleartext[16:32], 'deadbeef00000000decafbad00000000'.decode('hex')) # sha256(dummy_key).digest() eq_(cleartext[32:], ('47aedd050b9e19171f0fa7b8b65ca670' '28f0bc92cd3f2cd3682b1200ec759007').decode('hex')) return 'abcd123' self.fernet_mock.configure_mock(**{ 'encrypt.side_effect': mock_encrypt, }) self.reg.request.headers["Authorization"] = self.auth def handle_finish(value): call_args = self.reg.write.call_args ok_(call_args is not None) args = call_args[0] call_arg = json.loads(args[0]) eq_(call_arg["channelID"], dummy_chid.hex) eq_(call_arg["endpoint"], "http://localhost/wpush/v2/abcd123") def restore(*args, **kwargs): uuid.uuid4 = old_func old_func = uuid.uuid4 uuid.uuid4 = lambda: dummy_chid self.finish_deferred.addBoth(restore) 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)) return self.finish_deferred
def test_topic_replacement_delivery(self): data = str(uuid.uuid4()) data2 = str(uuid.uuid4()) client = yield self.quick_register() yield client.disconnect() yield client.send_notification(data=data, topic="Inbox", status=201) yield client.send_notification(data=data2, topic="Inbox", status=201) yield client.connect() yield client.hello() result = yield client.get_notification() # the following presumes that only `salt` is padded. clean_header = client._crypto_key.replace( '"', '').rstrip('=') assert result["headers"]["encryption"] == clean_header assert result["data"] == base64url_encode(data2) assert result["messageType"] == "notification" result = yield client.get_notification() assert result is None yield self.shut_down(client)
def test_no_delivery_to_unregistered(self): data = str(uuid.uuid4()) client = yield self.quick_register() # type: Client assert client.channels chan = client.channels.keys()[0] result = yield client.send_notification(data=data) assert result["channelID"] == chan assert result["data"] == base64url_encode(data) yield client.ack(result["channelID"], result["version"]) yield client.unregister(chan) result = yield client.send_notification(data=data, status=410) # Verify cache-control assert client.notif_response.getheader("Cache-Control") == \ "max-age=86400" assert result is None yield self.shut_down(client)
def test_no_delivery_to_unregistered_on_reconnect(self): data = str(uuid.uuid4()) client = yield self.quick_register(use_webpush=True) yield client.disconnect() ok_(client.channels) chan = client.channels.keys()[0] yield client.send_notification(data=data) yield client.connect() yield client.hello() result = yield client.get_notification() eq_(result["channelID"], chan) eq_(result["data"], base64url_encode(data)) yield client.unregister(chan) yield client.disconnect() time.sleep(1) yield client.connect() yield client.hello() result = yield client.get_notification() eq_(result, None) yield self.shut_down(client)
def test_ttl_batch_expired_and_good_one(self): data = str(uuid.uuid4()) data2 = str(uuid.uuid4()) client = yield self.quick_register() yield client.disconnect() for x in range(0, 12): yield client.send_notification(data=data, ttl=1, status=201) yield client.send_notification(data=data2, status=201) time.sleep(1) yield client.connect() yield client.hello() result = yield client.get_notification(timeout=4) assert result is not None # the following presumes that only `salt` is padded. clean_header = client._crypto_key.replace( '"', '').rstrip('=') assert result["headers"]["encryption"] == clean_header assert result["data"] == base64url_encode(data2) assert result["messageType"] == "notification" result = yield client.get_notification(timeout=0.5) assert result is None yield self.shut_down(client)
def _get_vapid(key=None, payload=None, endpoint=None): global CONNECTION_CONFIG if endpoint is None: endpoint = "{}://{}:{}".format( CONNECTION_CONFIG.get("endpoint_scheme"), CONNECTION_CONFIG.get("endpoint_hostname"), CONNECTION_CONFIG.get("endpoint_port"), ) if not payload: payload = {"aud": endpoint, "exp": int(time.time()) + 86400, "sub": "mailto:[email protected]"} if not payload.get("aud"): payload['aud'] = endpoint if not key: key = ecdsa.SigningKey.generate(curve=ecdsa.NIST256p) vk = key.get_verifying_key() auth = jws.sign(payload, key, algorithm="ES256").strip('=') crypto_key = base64url_encode('\4' + vk.to_string()) return {"auth": auth, "crypto-key": crypto_key, "key": key}
def _gen_jwt(self, header, payload): sk256p = ecdsa.SigningKey.generate(curve=ecdsa.NIST256p) vk = sk256p.get_verifying_key() sig = jws.sign(payload, sk256p, algorithm="ES256").strip('=') crypto_key = utils.base64url_encode(vk.to_string()).strip('=') return sig, crypto_key
def validate_auth(self, d): auth = d["headers"].get("authorization") needs_auth = d["token_info"]["api_ver"] == "v2" if not auth and not needs_auth: return public_key = d["subscription"].get("public_key") try: auth_type, token = auth.split(' ', 1) except ValueError: raise InvalidRequest("Invalid Authorization Header", status_code=401, errno=109, headers={"www-authenticate": PREF_SCHEME}) # If its not a bearer token containing what may be JWT, stop if auth_type.lower() not in AUTH_SCHEMES or '.' not in token: if needs_auth: raise InvalidRequest("Missing Authorization Header", status_code=401, errno=109) return try: jwt = extract_jwt(token, public_key) except (ValueError, InvalidSignature, TypeError): raise InvalidRequest("Invalid Authorization Header", status_code=401, errno=109, headers={"www-authenticate": PREF_SCHEME}) if "exp" not in jwt: raise InvalidRequest("Invalid bearer token: No expiration", status_code=401, errno=109, headers={"www-authenticate": PREF_SCHEME}) try: jwt_expires = int(jwt['exp']) except ValueError: raise InvalidRequest("Invalid bearer token: Invalid expiration", status_code=401, errno=109, headers={"www-authenticate": PREF_SCHEME}) now = time.time() jwt_has_expired = now > jwt_expires if jwt_has_expired: raise InvalidRequest("Invalid bearer token: Auth expired", status_code=401, errno=109, headers={"www-authenticate": PREF_SCHEME}) jwt_too_far_in_future = (jwt_expires - now) > (60 * 60 * 24) if jwt_too_far_in_future: raise InvalidRequest( "Invalid bearer token: Auth > 24 hours in " "the future", status_code=401, errno=109, headers={"www-authenticate": PREF_SCHEME}) jwt_crypto_key = base64url_encode(public_key) d["jwt"] = dict(jwt_crypto_key=jwt_crypto_key, jwt_data=jwt)
def validate_auth(self, d): crypto_exceptions = [ KeyError, ValueError, TypeError, VapidAuthException ] if self.context['conf'].use_cryptography: crypto_exceptions.append(InvalidSignature) else: crypto_exceptions.extend([JOSEError, JWTError, AssertionError]) auth = d["headers"].get("authorization") needs_auth = d["token_info"]["api_ver"] == "v2" if not needs_auth and not auth: return try: vapid_auth = parse_auth_header(auth) token = vapid_auth['t'] d["vapid_version"] = "draft{:0>2}".format(vapid_auth['version']) if vapid_auth['version'] == 2: public_key = vapid_auth['k'] else: public_key = d["subscription"].get("public_key") jwt = extract_jwt(token, public_key, is_trusted=self.context['conf'].enable_tls_auth, use_crypto=self.context['conf'].use_cryptography) if not isinstance(jwt, Dict): raise InvalidRequest("Invalid Authorization Header", status_code=401, errno=109, headers={"www-authenticate": PREF_SCHEME}) except tuple(crypto_exceptions): raise InvalidRequest("Invalid Authorization Header", status_code=401, errno=109, headers={"www-authenticate": PREF_SCHEME}) if "aud" not in jwt: raise InvalidRequest("Invalid bearer token: No Audience specified", status_code=401, errno=109, headers={"www-authenticate": PREF_SCHEME}) if jwt['aud'] != self.context["conf"].endpoint_url: raise InvalidRequest( "Invalid bearer token: Invalid Audience Specified", status_code=401, errno=109, headers={"www-authenticate": PREF_SCHEME}) if "exp" not in jwt: raise InvalidRequest("Invalid bearer token: No expiration", status_code=401, errno=109, headers={"www-authenticate": PREF_SCHEME}) try: jwt_expires = int(jwt['exp']) except (TypeError, ValueError): raise InvalidRequest("Invalid bearer token: Invalid expiration", status_code=401, errno=109, headers={"www-authenticate": PREF_SCHEME}) now = time.time() jwt_has_expired = now > jwt_expires if jwt_has_expired: raise InvalidRequest("Invalid bearer token: Auth expired", status_code=401, errno=109, headers={"www-authenticate": PREF_SCHEME}) jwt_too_far_in_future = (jwt_expires - now) > (60 * 60 * 24) if jwt_too_far_in_future: raise InvalidRequest( "Invalid bearer token: Auth > 24 hours in " "the future", status_code=401, errno=109, headers={"www-authenticate": PREF_SCHEME}) jwt_crypto_key = base64url_encode(public_key) d["jwt"] = dict(jwt_crypto_key=jwt_crypto_key, jwt_data=jwt)
def _uaid_lookup_results(self, result): """Process the result of the AWS UAID lookup""" # Save the whole record router_key = self.router_key = result.get("router_type", "simplepush") self._client_info["router_key"] = router_key try: self.router = self.ap_settings.routers[router_key] except KeyError: self.log.debug( format="Invalid router requested", status_code=400, errno=108, **self._client_info) return self._write_response(400, 108, message="Invalid router") # Only simplepush uses version/data out of body/query, GCM/APNS will # use data out of the request body 'WebPush' style. use_simplepush = router_key == "simplepush" if use_simplepush: self.version, data = parse_request_params(self.request) self._client_info['message_id'] = self.version else: data = self.request.body if "ttl" not in self.request.headers: ttl = None # We need crypto headers for messages with payloads. req_fields = ["content-encoding", "encryption"] if data and not all([x in self.request.headers 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) if ("encryption-key" in self.request.headers and "crypto-key" in self.request.headers): self.log.debug(format="Client error", status_code=400, errno=110, **self._client_info) return self._write_response( 400, 110, message="Invalid crypto headers") self._client_info["message_size"] = len(data) if data else 0 if "ttl" not in self.request.headers: ttl = None elif VALID_TTL.match(self.request.headers["ttl"]): ttl = int(self.request.headers["ttl"]) # Cap the TTL to our MAX_TTL ttl = min(ttl, MAX_TTL) else: self.log.debug(format="Client error", status_code=400, errno=112, **self._client_info) return self._write_response(400, 112, message="Invalid TTL header") if data and len(data) > self.ap_settings.max_data: self.log.debug(format="Client error", status_code=400, errno=104, **self._client_info) return self._write_response( 413, 104, message="Data payload too large") if use_simplepush: self._route_notification(self.version, result, data) return # Web Push and bridged messages are encrypted binary blobs. We store # and deliver these messages as Base64-encoded strings. data = base64url_encode(self.request.body) # Generate a message ID, then route the notification. d = deferToThread(self.ap_settings.fernet.encrypt, ':'.join([ 'm', self.uaid, self.chid]).encode('utf8')) d.addCallback(self._route_notification, result, data, ttl) return d
def _uaid_lookup_results(self, result): """Process the result of the AWS UAID lookup""" # Save the whole record router_key = self.router_key = result.get("router_type", "simplepush") self._client_info["router_key"] = router_key try: self.router = self.ap_settings.routers[router_key] except KeyError: self.log.debug("Invalid router requested", status_code=400, errno=108, **self._client_info) return self._write_response(400, 108, message="Invalid router") # Only simplepush uses version/data out of body/query, GCM/APNS will # use data out of the request body 'WebPush' style. use_simplepush = router_key == "simplepush" if use_simplepush: self.version, data = parse_request_params(self.request) self._client_info['message_id'] = self.version else: data = self.request.body if "ttl" not in self.request.headers: ttl = None # We need crypto headers for messages with payloads. req_fields = ["content-encoding", "encryption"] if data and not all( [x in self.request.headers for x in req_fields]): self.log.debug("Client error", status_code=400, errno=101, **self._client_info) return self._write_response(400, 101) if ("encryption-key" in self.request.headers and "crypto-key" in self.request.headers): self.log.debug("Client error", status_code=400, errno=110, **self._client_info) return self._write_response(400, 110, message="Invalid crypto headers") self._client_info["message_size"] = len(data) if data else 0 if "ttl" not in self.request.headers: ttl = None elif VALID_TTL.match(self.request.headers["ttl"]): ttl = int(self.request.headers["ttl"]) # Cap the TTL to our MAX_TTL ttl = min(ttl, MAX_TTL) else: self.log.debug("Client error", status_code=400, errno=112, **self._client_info) return self._write_response(400, 112, message="Invalid TTL header") if data and len(data) > self.ap_settings.max_data: self.log.debug("Client error", status_code=400, errno=104, **self._client_info) return self._write_response(413, 104, message="Data payload too large") if use_simplepush: self._route_notification(self.version, result, data) return # Web Push and bridged messages are encrypted binary blobs. We store # and deliver these messages as Base64-encoded strings. data = base64url_encode(self.request.body) # Generate a message ID, then route the notification. d = deferToThread(self.ap_settings.fernet.encrypt, ':'.join(['m', self.uaid, self.chid]).encode('utf8')) d.addCallback(self._route_notification, result, data, ttl) return d