def test_encode(self): recv_key = pyelliptic.ECC(curve="prime256v1") subscription_info = self._gen_subscription_info(recv_key) data = "Mary had a little lamb, with some nice mint jelly" push = WebPusher(subscription_info) encoded = push.encode(data) keyid = base64.urlsafe_b64encode(recv_key.get_pubkey()[1:]) http_ece.keys[keyid] = recv_key http_ece.labels[keyid] = 'P-256' # Convert these b64 strings into their raw, binary form. raw_salt = base64.urlsafe_b64decode(push._repad(encoded['salt'])) raw_dh = base64.urlsafe_b64decode(push._repad(encoded['crypto_key'])) raw_auth = base64.urlsafe_b64decode( push._repad(subscription_info['keys']['auth'])) decoded = http_ece.decrypt( buffer=encoded['body'], salt=raw_salt, dh=raw_dh, keyid=keyid, authSecret=raw_auth ) eq_(decoded, data)
def test_encode(self): for content_encoding in ["aesgcm", "aes128gcm"]: recv_key = ec.generate_private_key( ec.SECP256R1, default_backend()) subscription_info = self._gen_subscription_info(recv_key) data = "Mary had a little lamb, with some nice mint jelly" push = WebPusher(subscription_info) encoded = push.encode(data, content_encoding=content_encoding) """ crypto_key = base64.urlsafe_b64encode( self._get_pubkey_str(recv_key) ).strip(b'=') """ # Convert these b64 strings into their raw, binary form. raw_salt = None if 'salt' in encoded: raw_salt = base64.urlsafe_b64decode( push._repad(encoded['salt'])) raw_dh = None if content_encoding != "aes128gcm": raw_dh = base64.urlsafe_b64decode( push._repad(encoded['crypto_key'])) raw_auth = base64.urlsafe_b64decode( push._repad(subscription_info['keys']['auth'])) decoded = http_ece.decrypt( encoded['body'], salt=raw_salt, dh=raw_dh, private_key=recv_key, auth_secret=raw_auth, version=content_encoding ) eq_(decoded.decode('utf8'), data)
def send(reg_id, message, **kwargs): """ Site: https://developers.google.com API: https://developers.google.com/web/updates/2016/03/web-push-encryption Desc: Web Push notifications for Chrome and FireFox Installation: pip install 'pywebpush>=0.4.0' """ subscription_info = kwargs.pop('subscription_info') payload = { "title": kwargs.pop("event"), "body": message, "url": kwargs.pop("push_url", None) } payload.update(kwargs) wp = WebPusher(subscription_info) response = wp.send( dumps(payload), gcm_key=settings.GCM_KEY, ttl=kwargs.pop("ttl", 60)) if not response.ok or ( response.text and loads(response.text).get("failure") > 0): raise GCMError(response.text) return True
def queue_push_firefox(apids, message): payload_data = json.loads(message) ttl = 7 * 86400 apids_to_clear = [] for apid in apids: subscription = json.loads(apid.token) wp = WebPusher(subscription) response = wp.send(json.dumps(payload_data), ttl=ttl) if response.status_code == 410: apids_to_clear.append(apid) # Remove unregistered ids for apid in apids_to_clear: apid.delete()
def test_gcm(self, mock_post): subscription_info = self._gen_subscription_info( None, endpoint="https://android.googleapis.com/gcm/send/regid123") headers = {"Crypto-Key": "pre-existing", "Authentication": "bearer vapid"} data = "Mary had a little lamb" wp = WebPusher(subscription_info) wp.send(data, headers, gcm_key="gcm_key_value") pdata = json.loads(mock_post.call_args[1].get('data')) pheaders = mock_post.call_args[1].get('headers') eq_(pdata["registration_ids"][0], "regid123") eq_(pheaders.get("authorization"), "key=gcm_key_value") eq_(pheaders.get("content-type"), "application/json")
def queue_push_chrome(apids, message): payload_data = json.loads(message) ttl = 7 * 86400 apids_to_clear = [] for apid in apids: subscription = json.loads(apid.token) wp = WebPusher(subscription) response = wp.send(json.dumps(payload_data), gcm_key=settings.GCM_KEY, ttl=ttl) if response.status_code == 400: content = response.content if content.find("UnauthorizedRegistration") >= 0: apids_to_clear.append(apid) # Remove unregistered ids for apid in apids_to_clear: apid.delete()
def test_init(self): # use static values so we know what to look for in the reply subscription_info = { u"endpoint": u"https://example.com/", u"keys": { u"p256dh": (u"BOrnIslXrUow2VAzKCUAE4sIbK00daEZCswOcf8m3T" "F8V82B-OpOg5JbmYLg44kRcvQC1E2gMJshsUYA-_zMPR8"), u"auth": u"k8JV6sjdbhAi1n3_LDBLvA" } } rk_decode = (b'\x04\xea\xe7"\xc9W\xadJ0\xd9P3(%\x00\x13\x8b' b'\x08l\xad4u\xa1\x19\n\xcc\x0eq\xff&\xdd1' b'|W\xcd\x81\xf8\xeaN\x83\x92[\x99\x82\xe0\xe3' b'\x89\x11r\xf4\x02\xd4M\xa00\x9b!\xb1F\x00' b'\xfb\xfc\xcc=\x1f') self.assertRaises( WebPushException, WebPusher, {"keys": {'p256dh': 'AAA=', 'auth': 'AAA='}}) self.assertRaises( WebPushException, WebPusher, {"endpoint": "https://example.com", "keys": {'p256dh': 'AAA='}}) self.assertRaises( WebPushException, WebPusher, {"endpoint": "https://example.com", "keys": {'auth': 'AAA='}}) self.assertRaises( WebPushException, WebPusher, {"endpoint": "https://example.com", "keys": {'p256dh': 'AAA=', 'auth': 'AAA='}}) push = WebPusher(subscription_info) eq_(push.subscription_info, subscription_info) eq_(push.receiver_key, rk_decode) eq_(push.auth_key, b'\x93\xc2U\xea\xc8\xddn\x10"\xd6}\xff,0K\xbc')
def send_message(self, message="", **kwargs): """Send a message to a user.""" import jwt from pywebpush import WebPusher timestamp = int(time.time()) tag = str(uuid.uuid4()) payload = { 'badge': '/static/images/notification-badge.png', 'body': message, ATTR_DATA: {}, 'icon': '/static/icons/favicon-192x192.png', ATTR_TAG: tag, 'timestamp': (timestamp*1000), # Javascript ms since epoch ATTR_TITLE: kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) } data = kwargs.get(ATTR_DATA) if data: # Pick out fields that should go into the notification directly vs # into the notification data dictionary. data_tmp = {} for key, val in data.items(): if key in HTML5_SHOWNOTIFICATION_PARAMETERS: payload[key] = val else: data_tmp[key] = val payload[ATTR_DATA] = data_tmp if (payload[ATTR_DATA].get(ATTR_URL) is None and payload.get(ATTR_ACTIONS) is None): payload[ATTR_DATA][ATTR_URL] = URL_ROOT targets = kwargs.get(ATTR_TARGET) if not targets: targets = self.registrations.keys() for target in list(targets): info = self.registrations.get(target) if info is None: _LOGGER.error("%s is not a valid HTML5 push notification" " target", target) continue jwt_exp = (datetime.datetime.fromtimestamp(timestamp) + datetime.timedelta(days=JWT_VALID_DAYS)) jwt_secret = info[ATTR_SUBSCRIPTION][ATTR_KEYS][ATTR_AUTH] jwt_claims = {'exp': jwt_exp, 'nbf': timestamp, 'iat': timestamp, ATTR_TARGET: target, ATTR_TAG: payload[ATTR_TAG]} jwt_token = jwt.encode(jwt_claims, jwt_secret).decode('utf-8') payload[ATTR_DATA][ATTR_JWT] = jwt_token response = WebPusher(info[ATTR_SUBSCRIPTION]).send( json.dumps(payload), gcm_key=self._gcm_key, ttl='86400') # pylint: disable=no-member if response.status_code == 410: _LOGGER.info("Notification channel has expired") reg = self.registrations.pop(target) if not _save_config(self.registrations_json_path, self.registrations): self.registrations[target] = reg _LOGGER.error("Error saving registration.") else: _LOGGER.info("Configuration saved")
def _push_message(self, payload, **kwargs): """Send the message.""" from pywebpush import WebPusher timestamp = int(time.time()) ttl = int(kwargs.get(ATTR_TTL, DEFAULT_TTL)) priority = kwargs.get(ATTR_PRIORITY, DEFAULT_PRIORITY) if priority not in ['normal', 'high']: priority = DEFAULT_PRIORITY payload['timestamp'] = (timestamp * 1000) # Javascript ms since epoch targets = kwargs.get(ATTR_TARGET) if not targets: targets = self.registrations.keys() for target in list(targets): info = self.registrations.get(target) try: info = REGISTER_SCHEMA(info) except vol.Invalid: _LOGGER.error( "%s is not a valid HTML5 push notification" " target", target) continue payload[ATTR_DATA][ATTR_JWT] = add_jwt( timestamp, target, payload[ATTR_TAG], info[ATTR_SUBSCRIPTION][ATTR_KEYS][ATTR_AUTH]) import jwt jwt_secret = info[ATTR_SUBSCRIPTION][ATTR_KEYS][ATTR_AUTH] jwt_exp = (datetime.fromtimestamp(timestamp) + timedelta(days=JWT_VALID_DAYS)) jwt_claims = { 'exp': jwt_exp, 'nbf': timestamp, 'iat': timestamp, ATTR_TARGET: target, ATTR_TAG: payload[ATTR_TAG] } jwt_token = jwt.encode(jwt_claims, jwt_secret).decode('utf-8') payload[ATTR_DATA][ATTR_JWT] = jwt_token webpusher = WebPusher(info[ATTR_SUBSCRIPTION]) if self._vapid_prv and self._vapid_email: vapid_headers = create_vapid_headers(self._vapid_email, info[ATTR_SUBSCRIPTION], self._vapid_prv) vapid_headers.update({ 'urgency': priority, 'priority': priority }) response = webpusher.send(data=json.dumps(payload), headers=vapid_headers, ttl=ttl) else: # Only pass the gcm key if we're actually using GCM # If we don't, notifications break on FireFox gcm_key = self._gcm_key \ if 'googleapis.com' \ in info[ATTR_SUBSCRIPTION][ATTR_ENDPOINT] \ else None response = webpusher.send(json.dumps(payload), gcm_key=gcm_key, ttl=ttl) if response.status_code == 410: _LOGGER.info("Notification channel has expired") reg = self.registrations.pop(target) if not save_json(self.registrations_json_path, self.registrations): self.registrations[target] = reg _LOGGER.error("Error saving registration") else: _LOGGER.info("Configuration saved")
def test_encode_empty(self): subscription_info = self._gen_subscription_info() headers = {"Crypto-Key": "pre-existing", "Authentication": "bearer vapid"} encoded = WebPusher(subscription_info).encode('', headers) eq_(encoded, None)
"p256dh": "BK7h-R0UgDeT89jhWi76-FlTtlEr3DbVBnrr34qmK91Husli_Fazu7vo7kW1mg9F_qhNzrs2glbrc6wfqGFsXks=", "auth": "CyOHiGNXPcT5Slo9UMx2uA==" } } data = 'aaaaaa' vapid_private_key = PRIVATE_KEY = ''' MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg2xyYpqQhOaIdSbqH UwM3+ySvF47MoJyAFUNaHM7g/zOhRANCAAT554ztzCpjiIFOxNfEIicSzNPOZTIB Y1+CGl+LDfM5RlUNERFdfZYRqMmwvX7ydq7UiASkspWqdVVKZnLCzPD3 '''.strip() vapid_claims = {"sub": "mailto:[email protected]"} vapid_headers = None if vapid_claims: if not vapid_claims.get('aud'): url = urlparse(subscription_info.get('endpoint')) aud = "{}://{}".format(url.scheme, url.netloc) vapid_claims['aud'] = aud if os.path.isfile(vapid_private_key): vv = Vapid.from_file( private_key_file=vapid_private_key) # pragma no cover else: vv = Vapid.from_string(private_key=vapid_private_key) vapid_headers = vv.sign(vapid_claims) result = WebPusher(subscription_info, requests_session).send(data, vapid_headers) print result.text
for target in list(targets): info = self.registrations.get(target) try: info = REGISTER_SCHEMA(info) except vol.Invalid: _LOGGER.error( "%s is not a valid HTML5 push notification target", target) continue payload[ATTR_DATA][ATTR_JWT] = add_jwt( timestamp, target, payload[ATTR_TAG], info[ATTR_SUBSCRIPTION][ATTR_KEYS][ATTR_AUTH], ) webpusher = WebPusher(info[ATTR_SUBSCRIPTION]) if self._vapid_prv and self._vapid_email: vapid_headers = create_vapid_headers(self._vapid_email, info[ATTR_SUBSCRIPTION], self._vapid_prv) vapid_headers.update({ "urgency": priority, "priority": priority }) response = webpusher.send(data=json.dumps(payload), headers=vapid_headers, ttl=ttl) else: # Only pass the gcm key if we're actually using GCM # If we don't, notifications break on FireFox gcm_key = (self._gcm_key if "googleapis.com"
def push(self, message: str, icon: str = '/static/favicon.ico') -> None: WebPusher(self.to_dict()).send( json.dumps({'body': message, 'icon': icon}), gcm_key=GOOGLE_API_KEY)
print "Please provide a message" exit() else: print "\nMessage: " + args.message print "Title: " + args.title if not args.school: print "Sending to all schools" students = Student.objects.all() else: students = Student.objects.filter(personaltimetable__courses__school=args.school).distinct() | Student.objects.filter(school=args.school).distinct() print "Sending to <" + args.school + "> student(s)" tokens = RegistrationToken.objects.filter(student__in=students) print "This will send to ", tokens.count(), "device(s)" confirmation = raw_input("Would you like to send? (Y/n)") if confirmation.lower() != "y": print "Quitting...\n" exit() for token in tokens: subscription_info = {"endpoint":"https://android.googleapis.com/gcm/send/fuKkKlzSEEE:APA91bFKiuJ3LEx7Ke3xOEJ…Lx-t8INikH97ewASUn6OSzRAGeGf8Eu1B5Q7Lju_7QBj5VeGjwCePUufhiSzFXqEogaJ4esAqA","keys":{"p256dh":token.p256dh,"auth":token.auth}} data = '{"data": {"message":"' + args.message + '","title":"' + args.title + '"}}' encoded = WebPusher(subscription_info).encode(data) crypto_key = "dh=" + encoded["crypto_key"] salt = "salt=" + encoded['salt'] headers = {'Authorization': 'key=' + FCM_KEY, 'Content-Type': 'application/json', } headers.update({'crypto-key': crypto_key, 'content-encoding': 'aesgcm', 'encryption': salt}) fcm_data = {"raw_data": base64.b64encode(encoded.get('body')), "registration_ids": [token.endpoint[40:]]} resp = requests.post(FCM_URL, data=json.dumps(fcm_data), headers=headers)
def get_webpusher(subscription): '''Return a WebPusher instance for a given subscription.''' return WebPusher(subscription)
def send_message(self, message="", **kwargs): """Send a message to a user.""" import jwt from pywebpush import WebPusher timestamp = int(time.time()) tag = str(uuid.uuid4()) payload = { 'badge': '/static/images/notification-badge.png', 'body': message, ATTR_DATA: {}, 'icon': '/static/icons/favicon-192x192.png', ATTR_TAG: tag, 'timestamp': (timestamp * 1000), # Javascript ms since epoch ATTR_TITLE: kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) } data = kwargs.get(ATTR_DATA) if data: # Pick out fields that should go into the notification directly vs # into the notification data dictionary. for key, val in data.copy().items(): if key in HTML5_SHOWNOTIFICATION_PARAMETERS: payload[key] = val del data[key] payload[ATTR_DATA] = data if (payload[ATTR_DATA].get(ATTR_URL) is None and payload.get(ATTR_ACTIONS) is None): payload[ATTR_DATA][ATTR_URL] = URL_ROOT targets = kwargs.get(ATTR_TARGET) if not targets: targets = self.registrations.keys() elif not isinstance(targets, list): targets = [targets] for target in targets: info = self.registrations.get(target) if info is None: _LOGGER.error( '%s is not a valid HTML5 push notification' ' target!', target) continue jwt_exp = (datetime.datetime.fromtimestamp(timestamp) + datetime.timedelta(days=JWT_VALID_DAYS)) jwt_secret = info[ATTR_SUBSCRIPTION][ATTR_KEYS][ATTR_AUTH] jwt_claims = { 'exp': jwt_exp, 'nbf': timestamp, 'iat': timestamp, ATTR_TARGET: target, ATTR_TAG: payload[ATTR_TAG] } jwt_token = jwt.encode(jwt_claims, jwt_secret).decode('utf-8') payload[ATTR_DATA][ATTR_JWT] = jwt_token WebPusher(info[ATTR_SUBSCRIPTION]).send(json.dumps(payload), gcm_key=self._gcm_key, ttl='86400')
confirmation = raw_input("Would you like to send? (Y/n)") if confirmation.lower() != "y": print "Quitting...\n" exit() for token in tokens: subscription_info = { "endpoint": "https://android.googleapis.com/gcm/send/fuKkKlzSEEE:APA91bFKiuJ3LEx7Ke3xOEJ…Lx-t8INikH97ewASUn6OSzRAGeGf8Eu1B5Q7Lju_7QBj5VeGjwCePUufhiSzFXqEogaJ4esAqA", "keys": { "p256dh": token.p256dh, "auth": token.auth } } data = '{"data": {"message":"' + args.message + '","title":"' + args.title + '"}}' encoded = WebPusher(subscription_info).encode(data) crypto_key = "dh=" + encoded["crypto_key"] salt = "salt=" + encoded['salt'] headers = { 'Authorization': 'key=' + FCM_KEY, 'Content-Type': 'application/json', } headers.update({ 'crypto-key': crypto_key, 'content-encoding': 'aesgcm', 'encryption': salt }) fcm_data = { "raw_data": base64.b64encode(encoded.get('body')), "registration_ids": [token.endpoint[40:]] }
with open('config.json') as config_file: config = json.load(config_file) # Use grønstrøm API to get contents of push message message = requests.get('https://grønstrøm.nu/api/v1/next-day-short').text logging.info(f'Sending push message: {message}') # Get all subscriptions from SQLite database conn = sqlite3.Connection('/data/subs.db') try: c = conn.cursor() for i, row in enumerate(c.execute('SELECT * FROM subs')): try: # Manually recreate the push facade from the pywebpush API to be able to specify both TTL and urgency subscription_info = json.loads(row[0]) pusher = WebPusher(subscription_info) url = urlparse(subscription_info['endpoint']) aud = "{}://{}".format(url.scheme, url.netloc) vapid_claims = { 'sub': f'mailto:{config["sub_email"]}', 'aud': aud, 'exp': int(time.time()) + 12 * 60 * 60 } vv = Vapid.from_string(config['vapid_key']) headers = vv.sign(vapid_claims) # Define the urgency to be "normal", corresponding to messages being delivered # while the device is "On neither power nor wifi". # https://tools.ietf.org/html/draft-ietf-webpush-protocol-12#section-5.3 headers['Urgency'] = 'normal' resp = pusher.send(message, headers, ttl=12 * 60 * 60) # TODO: Handle cases where response status code is not 201.