Delete dist directory

This commit is contained in:
Mikahael 2024-02-25 23:59:01 +05:30 committed by GitHub
parent b2bfb6afd8
commit 9e59089810
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
1776 changed files with 0 additions and 548278 deletions

View file

@ -1,491 +0,0 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
import base64
from copy import deepcopy
import json
import os
import time
try:
from urllib.parse import urlparse
except ImportError: # pragma nocover
from urlparse import urlparse
import six
import http_ece
import requests
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives import serialization
from py_vapid import Vapid, Vapid01
class WebPushException(Exception):
"""Web Push failure.
This may contain the requests.Response
"""
def __init__(self, message, response=None):
self.message = message
self.response = response
def __str__(self):
extra = ""
if self.response:
try:
extra = ", Response {}".format(
self.response.text,
)
except AttributeError:
extra = ", Response {}".format(self.response)
return "WebPushException: {}{}".format(self.message, extra)
class CaseInsensitiveDict(dict):
"""A dictionary that has case-insensitive keys"""
def __init__(self, data={}, **kwargs):
for key in data:
dict.__setitem__(self, key.lower(), data[key])
self.update(kwargs)
def __contains__(self, key):
return dict.__contains__(self, key.lower())
def __setitem__(self, key, value):
dict.__setitem__(self, key.lower(), value)
def __getitem__(self, key):
return dict.__getitem__(self, key.lower())
def __delitem__(self, key):
dict.__delitem__(self, key.lower())
def get(self, key, default=None):
try:
return self.__getitem__(key)
except KeyError:
return default
def update(self, data):
for key in data:
self.__setitem__(key, data[key])
class WebPusher:
"""WebPusher encrypts a data block using HTTP Encrypted Content Encoding
for WebPush.
See https://tools.ietf.org/html/draft-ietf-webpush-protocol-04
for the current specification, and
https://developer.mozilla.org/en-US/docs/Web/API/Push_API for an
overview of Web Push.
Example of use:
The javascript promise handler for PushManager.subscribe()
receives a subscription_info object. subscription_info.getJSON()
will return a JSON representation.
(e.g.
.. code-block:: javascript
subscription_info.getJSON() ==
{"endpoint": "https://push.server.com/...",
"keys":{"auth": "...", "p256dh": "..."}
}
)
This subscription_info block can be stored.
To send a subscription update:
.. code-block:: python
# Optional
# headers = py_vapid.sign({"aud": "https://push.server.com/",
"sub": "mailto:your_admin@your.site.com"})
data = "Mary had a little lamb, with a nice mint jelly"
WebPusher(subscription_info).send(data, headers)
"""
subscription_info = {}
valid_encodings = [
# "aesgcm128", # this is draft-0, but DO NOT USE.
"aesgcm", # draft-httpbis-encryption-encoding-01
"aes128gcm" # RFC8188 Standard encoding
]
verbose = False
def __init__(self, subscription_info, requests_session=None,
verbose=False):
"""Initialize using the info provided by the client PushSubscription
object (See
https://developer.mozilla.org/en-US/docs/Web/API/PushManager/subscribe)
:param subscription_info: a dict containing the subscription_info from
the client.
:type subscription_info: dict
:param requests_session: a requests.Session object to optimize requests
to the same client.
:type requests_session: requests.Session
:param verbose: provide verbose feedback
:type verbose: bool
"""
self.verbose = verbose
if requests_session is None:
self.requests_method = requests
else:
self.requests_method = requests_session
if 'endpoint' not in subscription_info:
raise WebPushException("subscription_info missing endpoint URL")
self.subscription_info = deepcopy(subscription_info)
self.auth_key = self.receiver_key = None
if 'keys' in subscription_info:
keys = self.subscription_info['keys']
for k in ['p256dh', 'auth']:
if keys.get(k) is None:
raise WebPushException("Missing keys value: {}".format(k))
if isinstance(keys[k], six.text_type):
keys[k] = bytes(keys[k].encode('utf8'))
receiver_raw = base64.urlsafe_b64decode(
self._repad(keys['p256dh']))
if len(receiver_raw) != 65 and receiver_raw[0] != "\x04":
raise WebPushException("Invalid p256dh key specified")
self.receiver_key = receiver_raw
self.auth_key = base64.urlsafe_b64decode(
self._repad(keys['auth']))
def verb(self, msg, *args, **kwargs):
if self.verbose:
print(msg.format(*args, **kwargs))
def _repad(self, data):
"""Add base64 padding to the end of a string, if required"""
return data + b"===="[:len(data) % 4]
def encode(self, data, content_encoding="aes128gcm"):
"""Encrypt the data.
:param data: A serialized block of byte data (String, JSON, bit array,
etc.) Make sure that whatever you send, your client knows how
to understand it.
:type data: str
:param content_encoding: The content_encoding type to use to encrypt
the data. Defaults to RFC8188 "aes128gcm". The previous draft-01 is
"aesgcm", however this format is now deprecated.
:type content_encoding: enum("aesgcm", "aes128gcm")
"""
# Salt is a random 16 byte array.
if not data:
self.verb("No data found...")
return
if not self.auth_key or not self.receiver_key:
raise WebPushException("No keys specified in subscription info")
self.verb("Encoding data...")
salt = None
if content_encoding not in self.valid_encodings:
raise WebPushException("Invalid content encoding specified. "
"Select from " +
json.dumps(self.valid_encodings))
if content_encoding == "aesgcm":
self.verb("Generating salt for aesgcm...")
salt = os.urandom(16)
# The server key is an ephemeral ECDH key used only for this
# transaction
server_key = ec.generate_private_key(ec.SECP256R1, default_backend())
crypto_key = server_key.public_key().public_bytes(
encoding=serialization.Encoding.X962,
format=serialization.PublicFormat.UncompressedPoint
)
if isinstance(data, six.text_type):
data = bytes(data.encode('utf8'))
if content_encoding == "aes128gcm":
self.verb("Encrypting to aes128gcm...")
encrypted = http_ece.encrypt(
data,
salt=salt,
private_key=server_key,
dh=self.receiver_key,
auth_secret=self.auth_key,
version=content_encoding)
reply = CaseInsensitiveDict({
'body': encrypted
})
else:
self.verb("Encrypting to aesgcm...")
crypto_key = base64.urlsafe_b64encode(crypto_key).strip(b'=')
encrypted = http_ece.encrypt(
data,
salt=salt,
private_key=server_key,
keyid=crypto_key.decode(),
dh=self.receiver_key,
auth_secret=self.auth_key,
version=content_encoding)
reply = CaseInsensitiveDict({
'crypto_key': crypto_key,
'body': encrypted,
})
if salt:
reply['salt'] = base64.urlsafe_b64encode(salt).strip(b'=')
return reply
def as_curl(self, endpoint, encoded_data, headers):
"""Return the send as a curl command.
Useful for debugging. This will write out the encoded data to a local
file named `encrypted.data`
:param endpoint: Push service endpoint URL
:type endpoint: basestring
:param encoded_data: byte array of encoded data
:type encoded_data: bytearray
:param headers: Additional headers for the send
:type headers: dict
:returns string
"""
header_list = [
'-H "{}: {}" \\ \n'.format(
key.lower(), val) for key, val in headers.items()
]
data = ""
if encoded_data:
with open("encrypted.data", "wb") as f:
f.write(encoded_data)
data = "--data-binary @encrypted.data"
if 'content-length' not in headers:
self.verb("Generating content-length header...")
header_list.append(
'-H "content-length: {}" \\ \n'.format(len(encoded_data)))
return ("""curl -vX POST {url} \\\n{headers}{data}""".format(
url=endpoint, headers="".join(header_list), data=data))
def send(self, data=None, headers=None, ttl=0, gcm_key=None, reg_id=None,
content_encoding="aes128gcm", curl=False, timeout=None):
"""Encode and send the data to the Push Service.
:param data: A serialized block of data (see encode() ).
:type data: str
:param headers: A dictionary containing any additional HTTP headers.
:type headers: dict
:param ttl: The Time To Live in seconds for this message if the
recipient is not online. (Defaults to "0", which discards the
message immediately if the recipient is unavailable.)
:type ttl: int
:param gcm_key: API key obtained from the Google Developer Console.
Needed if endpoint is https://android.googleapis.com/gcm/send
:type gcm_key: string
:param reg_id: registration id of the recipient. If not provided,
it will be extracted from the endpoint.
:type reg_id: str
:param content_encoding: ECE content encoding (defaults to "aes128gcm")
:type content_encoding: str
:param curl: Display output as `curl` command instead of sending
:type curl: bool
:param timeout: POST requests timeout
:type timeout: float or tuple
"""
# Encode the data.
if headers is None:
headers = dict()
encoded = {}
headers = CaseInsensitiveDict(headers)
if data:
encoded = self.encode(data, content_encoding)
if "crypto_key" in encoded:
# Append the p256dh to the end of any existing crypto-key
crypto_key = headers.get("crypto-key", "")
if crypto_key:
# due to some confusion by a push service provider, we
# should use ';' instead of ',' to append the headers.
# see
# https://github.com/webpush-wg/webpush-encryption/issues/6
crypto_key += ';'
crypto_key += (
"dh=" + encoded["crypto_key"].decode('utf8'))
headers.update({
'crypto-key': crypto_key
})
if "salt" in encoded:
headers.update({
'encryption': "salt=" + encoded['salt'].decode('utf8')
})
headers.update({
'content-encoding': content_encoding,
})
if gcm_key:
# guess if it is a legacy GCM project key or actual FCM key
# gcm keys are all about 40 chars (use 100 for confidence),
# fcm keys are 153-175 chars
if len(gcm_key) < 100:
self.verb("Guessing this is legacy GCM...")
endpoint = 'https://android.googleapis.com/gcm/send'
else:
self.verb("Guessing this is FCM...")
endpoint = 'https://fcm.googleapis.com/fcm/send'
reg_ids = []
if not reg_id:
reg_id = self.subscription_info['endpoint'].rsplit('/', 1)[-1]
self.verb("Fetching out registration id: {}", reg_id)
reg_ids.append(reg_id)
gcm_data = dict()
gcm_data['registration_ids'] = reg_ids
if data:
gcm_data['raw_data'] = base64.b64encode(
encoded.get('body')).decode('utf8')
gcm_data['time_to_live'] = int(
headers['ttl'] if 'ttl' in headers else ttl)
encoded_data = json.dumps(gcm_data)
headers.update({
'Authorization': 'key='+gcm_key,
'Content-Type': 'application/json',
})
else:
encoded_data = encoded.get('body')
endpoint = self.subscription_info['endpoint']
if 'ttl' not in headers or ttl:
self.verb("Generating TTL of 0...")
headers['ttl'] = str(ttl or 0)
# Additionally useful headers:
# Authorization / Crypto-Key (VAPID headers)
if curl:
return self.as_curl(endpoint, encoded_data, headers)
self.verb("\nSending request to"
"\n\thost: {}\n\theaders: {}\n\tdata: {}",
endpoint, headers, encoded_data)
resp = self.requests_method.post(endpoint,
data=encoded_data,
headers=headers,
timeout=timeout)
self.verb("\nResponse:\n\tcode: {}\n\tbody: {}\n",
resp.status_code, resp.text or "Empty")
return resp
def webpush(subscription_info,
data=None,
vapid_private_key=None,
vapid_claims=None,
content_encoding="aes128gcm",
curl=False,
timeout=None,
ttl=0,
verbose=False,
headers=None,
requests_session=None):
"""
One call solution to endcode and send `data` to the endpoint
contained in `subscription_info` using optional VAPID auth headers.
in example:
.. code-block:: python
from pywebpush import python
webpush(
subscription_info={
"endpoint": "https://push.example.com/v1/abcd",
"keys": {"p256dh": "0123abcd...",
"auth": "001122..."}
},
data="Mary had a little lamb, with a nice mint jelly",
vapid_private_key="path/to/key.pem",
vapid_claims={"sub": "YourNameHere@example.com"}
)
No additional method call is required. Any non-success will throw a
`WebPushException`.
:param subscription_info: Provided by the client call
:type subscription_info: dict
:param data: Serialized data to send
:type data: str
:param vapid_private_key: Vapid instance or path to vapid private key PEM \
or encoded str
:type vapid_private_key: Union[Vapid, str]
:param vapid_claims: Dictionary of claims ('sub' required)
:type vapid_claims: dict
:param content_encoding: Optional content type string
:type content_encoding: str
:param curl: Return as "curl" string instead of sending
:type curl: bool
:param timeout: POST requests timeout
:type timeout: float or tuple
:param ttl: Time To Live
:type ttl: int
:param verbose: Provide verbose feedback
:type verbose: bool
:return requests.Response or string
:param headers: Dictionary of extra HTTP headers to include
:type headers: dict
"""
if headers is None:
headers = dict()
else:
# Ensure we don't leak VAPID headers by mutating the passed in dict.
headers = headers.copy()
vapid_headers = None
if vapid_claims:
if verbose:
print("Generating VAPID headers...")
if not vapid_claims.get('aud'):
url = urlparse(subscription_info.get('endpoint'))
aud = "{}://{}".format(url.scheme, url.netloc)
vapid_claims['aud'] = aud
# Remember, passed structures are mutable in python.
# It's possible that a previously set `exp` field is no longer valid.
if (not vapid_claims.get('exp')
or vapid_claims.get('exp') < int(time.time())):
# encryption lives for 12 hours
vapid_claims['exp'] = int(time.time()) + (12 * 60 * 60)
if verbose:
print("Setting VAPID expry to {}...".format(
vapid_claims['exp']))
if not vapid_private_key:
raise WebPushException("VAPID dict missing 'private_key'")
if isinstance(vapid_private_key, Vapid01):
vv = vapid_private_key
elif os.path.isfile(vapid_private_key):
# Presume that key from file is handled correctly by
# py_vapid.
vv = Vapid.from_file(
private_key_file=vapid_private_key) # pragma no cover
else:
vv = Vapid.from_string(private_key=vapid_private_key)
if verbose:
print("\t claims: {}".format(vapid_claims))
vapid_headers = vv.sign(vapid_claims)
if verbose:
print("\t headers: {}".format(vapid_headers))
headers.update(vapid_headers)
response = WebPusher(
subscription_info, requests_session=requests_session, verbose=verbose
).send(
data,
headers,
ttl=ttl,
content_encoding=content_encoding,
curl=curl,
timeout=timeout,
)
if not curl and response.status_code > 202:
raise WebPushException("Push failed: {} {}\nResponse body:{}".format(
response.status_code, response.reason, response.text),
response=response)
return response

View file

@ -1,67 +0,0 @@
import argparse
import os
import json
from pywebpush import webpush
def get_config():
parser = argparse.ArgumentParser(description="WebPush tool")
parser.add_argument("--data", '-d', help="Data file")
parser.add_argument("--info", "-i", help="Subscription Info JSON file")
parser.add_argument("--head", help="Header Info JSON file")
parser.add_argument("--claims", help="Vapid claim file")
parser.add_argument("--key", help="Vapid private key file path")
parser.add_argument("--curl", help="Don't send, display as curl command",
default=False, action="store_true")
parser.add_argument("--encoding", default="aes128gcm")
parser.add_argument("--verbose", "-v", help="Provide verbose feedback",
default=False, action="store_true")
args = parser.parse_args()
if not args.info:
raise Exception("Subscription Info argument missing.")
if not os.path.exists(args.info):
raise Exception("Subscription Info file missing.")
try:
with open(args.info) as r:
args.sub_info = json.loads(r.read())
if args.data:
with open(args.data) as r:
args.data = r.read()
if args.head:
with open(args.head) as r:
args.head = json.loads(r.read())
if args.claims:
if not args.key:
raise Exception("No private --key specified for claims")
with open(args.claims) as r:
args.claims = json.loads(r.read())
except Exception as ex:
print("Couldn't read input {}.".format(ex))
raise ex
return args
def main():
""" Send data """
try:
args = get_config()
result = webpush(
args.sub_info,
data=args.data,
vapid_private_key=args.key,
vapid_claims=args.claims,
curl=args.curl,
content_encoding=args.encoding,
verbose=args.verbose,
headers=args.head)
print(result)
except Exception as ex:
print("ERROR: {}".format(ex))
if __name__ == "__main__":
main()

View file

@ -1,390 +0,0 @@
import base64
import json
import os
import unittest
import time
from mock import patch, Mock
import http_ece
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.backends import default_backend
import py_vapid
from pywebpush import WebPusher, WebPushException, CaseInsensitiveDict, webpush
class WebpushTestCase(unittest.TestCase):
# This is a exported DER formatted string of an ECDH public key
# This was lifted from the py_vapid tests.
vapid_key = (
"MHcCAQEEIPeN1iAipHbt8+/KZ2NIF8NeN24jqAmnMLFZEMocY8RboAoGCCqGSM49"
"AwEHoUQDQgAEEJwJZq/GN8jJbo1GGpyU70hmP2hbWAUpQFKDByKB81yldJ9GTklB"
"M5xqEwuPM7VuQcyiLDhvovthPIXx+gsQRQ=="
)
def _gen_subscription_info(self,
recv_key=None,
endpoint="https://example.com/"):
if not recv_key:
recv_key = ec.generate_private_key(ec.SECP256R1, default_backend())
return {
"endpoint": endpoint,
"keys": {
'auth': base64.urlsafe_b64encode(os.urandom(16)).strip(b'='),
'p256dh': self._get_pubkey_str(recv_key),
}
}
def _get_pubkey_str(self, priv_key):
return base64.urlsafe_b64encode(
priv_key.public_key().public_bytes(
encoding=serialization.Encoding.X962,
format=serialization.PublicFormat.UncompressedPoint
)).strip(b'=')
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)
assert push.subscription_info != subscription_info
assert push.subscription_info['keys'] != subscription_info['keys']
assert push.subscription_info['endpoint'] == subscription_info['endpoint']
assert push.receiver_key == rk_decode
assert push.auth_key == b'\x93\xc2U\xea\xc8\xddn\x10"\xd6}\xff,0K\xbc'
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
)
assert decoded.decode('utf8') == data
def test_bad_content_encoding(self):
subscription_info = self._gen_subscription_info()
data = "Mary had a little lamb, with some nice mint jelly"
push = WebPusher(subscription_info)
self.assertRaises(WebPushException,
push.encode,
data,
content_encoding="aesgcm128")
@patch("requests.post")
def test_send(self, mock_post):
subscription_info = self._gen_subscription_info()
headers = {"Crypto-Key": "pre-existing",
"Authentication": "bearer vapid"}
data = "Mary had a little lamb"
WebPusher(subscription_info).send(data, headers)
assert subscription_info.get('endpoint') == mock_post.call_args[0][0]
pheaders = mock_post.call_args[1].get('headers')
assert pheaders.get('ttl') == '0'
assert pheaders.get('AUTHENTICATION') == headers.get('Authentication')
ckey = pheaders.get('crypto-key')
assert 'pre-existing' in ckey
assert pheaders.get('content-encoding') == 'aes128gcm'
@patch("requests.post")
def test_send_vapid(self, mock_post):
mock_post.return_value.status_code = 200
subscription_info = self._gen_subscription_info()
data = "Mary had a little lamb"
webpush(
subscription_info=subscription_info,
data=data,
vapid_private_key=self.vapid_key,
vapid_claims={"sub": "mailto:ops@example.com"},
content_encoding="aesgcm",
headers={"Test-Header": "test-value"}
)
assert subscription_info.get('endpoint') == mock_post.call_args[0][0]
pheaders = mock_post.call_args[1].get('headers')
assert pheaders.get('ttl') == '0'
def repad(str):
return str + "===="[:len(str) % 4]
auth = json.loads(
base64.urlsafe_b64decode(
repad(pheaders['authorization'].split('.')[1])
).decode('utf8')
)
assert subscription_info.get('endpoint').startswith(auth['aud'])
assert 'vapid' in pheaders.get('authorization')
ckey = pheaders.get('crypto-key')
assert 'dh=' in ckey
assert pheaders.get('content-encoding') == 'aesgcm'
assert pheaders.get('test-header') == 'test-value'
@patch.object(WebPusher, "send")
@patch.object(py_vapid.Vapid, "sign")
def test_webpush_vapid_instance(self, vapid_sign, pusher_send):
pusher_send.return_value.status_code = 200
subscription_info = self._gen_subscription_info()
data = "Mary had a little lamb"
vapid_key = py_vapid.Vapid.from_string(self.vapid_key)
claims = dict(sub="mailto:ops@example.com", aud="https://example.com")
webpush(
subscription_info=subscription_info,
data=data,
vapid_private_key=vapid_key,
vapid_claims=claims,
)
vapid_sign.assert_called_once_with(claims)
pusher_send.assert_called_once()
@patch.object(WebPusher, "send")
@patch.object(py_vapid.Vapid, "sign")
def test_webpush_vapid_exp(self, vapid_sign, pusher_send):
pusher_send.return_value.status_code = 200
subscription_info = self._gen_subscription_info()
data = "Mary had a little lamb"
vapid_key = py_vapid.Vapid.from_string(self.vapid_key)
claims = dict(sub="mailto:ops@example.com",
aud="https://example.com",
exp=int(time.time() - 48600))
webpush(
subscription_info=subscription_info,
data=data,
vapid_private_key=vapid_key,
vapid_claims=claims,
)
vapid_sign.assert_called_once_with(claims)
pusher_send.assert_called_once()
assert claims['exp'] > int(time.time())
@patch("requests.post")
def test_send_bad_vapid_no_key(self, mock_post):
mock_post.return_value.status_code = 200
subscription_info = self._gen_subscription_info()
data = "Mary had a little lamb"
self.assertRaises(
WebPushException,
webpush,
subscription_info=subscription_info,
data=data,
vapid_claims={
"aud": "https://example.com",
"sub": "mailto:ops@example.com"
})
@patch("requests.post")
def test_send_bad_vapid_bad_return(self, mock_post):
mock_post.return_value.status_code = 410
subscription_info = self._gen_subscription_info()
data = "Mary had a little lamb"
self.assertRaises(
WebPushException,
webpush,
subscription_info=subscription_info,
data=data,
vapid_claims={
"aud": "https://example.com",
"sub": "mailto:ops@example.com"
},
vapid_private_key=self.vapid_key)
@patch("requests.post")
def test_send_empty(self, mock_post):
subscription_info = self._gen_subscription_info()
headers = {"Crypto-Key": "pre-existing",
"Authentication": "bearer vapid"}
WebPusher(subscription_info).send('', headers)
assert subscription_info.get('endpoint') == mock_post.call_args[0][0]
pheaders = mock_post.call_args[1].get('headers')
assert pheaders.get('ttl') == '0'
assert 'encryption' not in pheaders
assert pheaders.get('AUTHENTICATION') == headers.get('Authentication')
ckey = pheaders.get('crypto-key')
assert 'pre-existing' in ckey
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)
assert encoded is None
def test_encode_no_crypto(self):
subscription_info = self._gen_subscription_info()
del(subscription_info['keys'])
headers = {"Crypto-Key": "pre-existing",
"Authentication": "bearer vapid"}
data = 'Something'
pusher = WebPusher(subscription_info)
self.assertRaises(
WebPushException,
pusher.encode,
data,
headers)
@patch("requests.post")
def test_send_no_headers(self, mock_post):
subscription_info = self._gen_subscription_info()
data = "Mary had a little lamb"
WebPusher(subscription_info).send(data)
assert subscription_info.get('endpoint') == mock_post.call_args[0][0]
pheaders = mock_post.call_args[1].get('headers')
assert pheaders.get('ttl') == '0'
assert pheaders.get('content-encoding') == 'aes128gcm'
@patch("pywebpush.open")
def test_as_curl(self, opener):
subscription_info = self._gen_subscription_info()
result = webpush(
subscription_info,
data="Mary had a little lamb",
vapid_claims={
"aud": "https://example.com",
"sub": "mailto:ops@example.com"
},
vapid_private_key=self.vapid_key,
curl=True
)
for s in [
"curl -vX POST https://example.com",
"-H \"content-encoding: aes128gcm\"",
"-H \"authorization: vapid ",
"-H \"ttl: 0\"",
"-H \"content-length:"
]:
assert s in result, "missing: {}".format(s)
def test_ci_dict(self):
ci = CaseInsensitiveDict({"Foo": "apple", "bar": "banana"})
assert 'apple' == ci["foo"]
assert 'apple' == ci.get("FOO")
assert 'apple' == ci.get("Foo")
del (ci['FOO'])
assert ci.get('Foo') is None
@patch("requests.post")
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')
assert pdata["registration_ids"][0] == "regid123"
assert pheaders.get("authorization") == "key=gcm_key_value"
assert pheaders.get("content-type") == "application/json"
@patch("requests.post")
def test_timeout(self, mock_post):
mock_post.return_value.status_code = 200
subscription_info = self._gen_subscription_info()
WebPusher(subscription_info).send(timeout=5.2)
assert mock_post.call_args[1].get('timeout') == 5.2
webpush(subscription_info, timeout=10.001)
assert mock_post.call_args[1].get('timeout') == 10.001
@patch("requests.Session")
def test_send_using_requests_session(self, mock_session):
subscription_info = self._gen_subscription_info()
headers = {"Crypto-Key": "pre-existing",
"Authentication": "bearer vapid"}
data = "Mary had a little lamb"
WebPusher(subscription_info,
requests_session=mock_session).send(data, headers)
assert subscription_info.get(
'endpoint') == mock_session.post.call_args[0][0]
pheaders = mock_session.post.call_args[1].get('headers')
assert pheaders.get('ttl') == '0'
assert pheaders.get('AUTHENTICATION') == headers.get('Authentication')
ckey = pheaders.get('crypto-key')
assert 'pre-existing' in ckey
assert pheaders.get('content-encoding') == 'aes128gcm'
class WebpushExceptionTestCase(unittest.TestCase):
def test_exception(self):
from requests import Response
exp = WebPushException("foo")
assert ("{}".format(exp) == "WebPushException: foo")
# Really should try to load the response to verify, but this mock
# covers what we need.
response = Mock(spec=Response)
response.text = (
'{"code": 401, "errno": 109, "error": '
'"Unauthorized", "more_info": "http://'
'autopush.readthedocs.io/en/latest/htt'
'p.html#error-codes", "message": "Requ'
'est did not validate missing authoriz'
'ation header"}')
response.json.return_value = json.loads(response.text)
response.status_code = 401
response.reason = "Unauthorized"
exp = WebPushException("foo", response)
assert "{}".format(exp) == "WebPushException: foo, Response {}".format(
response.text)
assert '{}'.format(exp.response), '<Response [401]>'
assert exp.response.json().get('errno') == 109
exp = WebPushException("foo", [1, 2, 3])
assert '{}'.format(exp) == "WebPushException: foo, Response [1, 2, 3]"