Skip to content

Commit 10e2e16

Browse files
authored
feat: Add --verbose flag (#117)
Closes: #116
1 parent 9bc6f69 commit 10e2e16

File tree

5 files changed

+58
-13
lines changed

5 files changed

+58
-13
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# I am terrible at keeping this up-to-date.
22

3+
## 1.10.0 (2019-08-13)
4+
feat: Add `--verbose` flag with some initial commentary
5+
bug: Update tests to use latest VAPID version
6+
37
## 1.9.4 (2019-05-09)
48
bug: update vapid `exp` header if missing or expired
59

pywebpush/__init__.py

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -116,8 +116,10 @@ class WebPusher:
116116
"aesgcm", # draft-httpbis-encryption-encoding-01
117117
"aes128gcm" # RFC8188 Standard encoding
118118
]
119+
verbose = False
119120

120-
def __init__(self, subscription_info, requests_session=None):
121+
def __init__(self, subscription_info, requests_session=None,
122+
verbose=False):
121123
"""Initialize using the info provided by the client PushSubscription
122124
object (See
123125
https://developer.mozilla.org/en-US/docs/Web/API/PushManager/subscribe)
@@ -130,7 +132,12 @@ def __init__(self, subscription_info, requests_session=None):
130132
to the same client.
131133
:type requests_session: requests.Session
132134
135+
:param verbose: provide verbose feedback
136+
:type verbose: bool
137+
133138
"""
139+
140+
self.verbose = verbose
134141
if requests_session is None:
135142
self.requests_method = requests
136143
else:
@@ -155,6 +162,10 @@ def __init__(self, subscription_info, requests_session=None):
155162
self.auth_key = base64.urlsafe_b64decode(
156163
self._repad(keys['auth']))
157164

165+
def verb(self, msg, *args, **kwargs):
166+
if self.verbose:
167+
print(msg.format(*args, **kwargs))
168+
158169
def _repad(self, data):
159170
"""Add base64 padding to the end of a string, if required"""
160171
return data + b"===="[:len(data) % 4]
@@ -174,15 +185,18 @@ def encode(self, data, content_encoding="aes128gcm"):
174185
"""
175186
# Salt is a random 16 byte array.
176187
if not data:
188+
self.verb("No data found...")
177189
return
178190
if not self.auth_key or not self.receiver_key:
179191
raise WebPushException("No keys specified in subscription info")
192+
self.verb("Encoding data...")
180193
salt = None
181194
if content_encoding not in self.valid_encodings:
182195
raise WebPushException("Invalid content encoding specified. "
183196
"Select from " +
184197
json.dumps(self.valid_encodings))
185198
if content_encoding == "aesgcm":
199+
self.verb("Generating salt for aesgcm...")
186200
salt = os.urandom(16)
187201
# The server key is an ephemeral ECDH key used only for this
188202
# transaction
@@ -195,6 +209,7 @@ def encode(self, data, content_encoding="aes128gcm"):
195209
if isinstance(data, six.string_types):
196210
data = bytes(data.encode('utf8'))
197211
if content_encoding == "aes128gcm":
212+
self.verb("Encrypting to aes128gcm...")
198213
encrypted = http_ece.encrypt(
199214
data,
200215
salt=salt,
@@ -206,6 +221,7 @@ def encode(self, data, content_encoding="aes128gcm"):
206221
'body': encrypted
207222
})
208223
else:
224+
self.verb("Encrypting to aesgcm...")
209225
crypto_key = base64.urlsafe_b64encode(crypto_key).strip(b'=')
210226
encrypted = http_ece.encrypt(
211227
data,
@@ -248,6 +264,7 @@ def as_curl(self, endpoint, encoded_data, headers):
248264
f.write(encoded_data)
249265
data = "--data-binary @encrypted.data"
250266
if 'content-length' not in headers:
267+
self.verb("Generating content-length header...")
251268
header_list.append(
252269
'-H "content-length: {}" \\ \n'.format(len(encoded_data)))
253270
return ("""curl -vX POST {url} \\\n{headers}{data}""".format(
@@ -312,12 +329,15 @@ def send(self, data=None, headers=None, ttl=0, gcm_key=None, reg_id=None,
312329
# gcm keys are all about 40 chars (use 100 for confidence),
313330
# fcm keys are 153-175 chars
314331
if len(gcm_key) < 100:
332+
self.verb("Guessing this is legacy GCM...")
315333
endpoint = 'https://android.googleapis.com/gcm/send'
316334
else:
335+
self.verb("Guessing this is FCM...")
317336
endpoint = 'https://fcm.googleapis.com/fcm/send'
318337
reg_ids = []
319338
if not reg_id:
320339
reg_id = self.subscription_info['endpoint'].rsplit('/', 1)[-1]
340+
self.verb("Fetching out registration id: {}", reg_id)
321341
reg_ids.append(reg_id)
322342
gcm_data = dict()
323343
gcm_data['registration_ids'] = reg_ids
@@ -336,15 +356,22 @@ def send(self, data=None, headers=None, ttl=0, gcm_key=None, reg_id=None,
336356
endpoint = self.subscription_info['endpoint']
337357

338358
if 'ttl' not in headers or ttl:
359+
self.verb("Generating TTL of 0...")
339360
headers['ttl'] = str(ttl or 0)
340361
# Additionally useful headers:
341362
# Authorization / Crypto-Key (VAPID headers)
342363
if curl:
343364
return self.as_curl(endpoint, encoded_data, headers)
344-
return self.requests_method.post(endpoint,
365+
self.verb("\nSending request to"
366+
"\n\thost: {}\n\theaders: {}\n\tdata: {}",
367+
endpoint, headers, encoded_data)
368+
resp = self.requests_method.post(endpoint,
345369
data=encoded_data,
346370
headers=headers,
347371
timeout=timeout)
372+
self.verb("\nResponse:\n\tcode: {}\n\tbody: {}\n",
373+
resp.status_code, resp.text or "Empty")
374+
return resp
348375

349376

350377
def webpush(subscription_info,
@@ -354,7 +381,8 @@ def webpush(subscription_info,
354381
content_encoding="aes128gcm",
355382
curl=False,
356383
timeout=None,
357-
ttl=0):
384+
ttl=0,
385+
verbose=False):
358386
"""
359387
One call solution to endcode and send `data` to the endpoint
360388
contained in `subscription_info` using optional VAPID auth headers.
@@ -396,11 +424,15 @@ def webpush(subscription_info,
396424
:type timeout: float or tuple
397425
:param ttl: Time To Live
398426
:type ttl: int
427+
:param verbose: Provide verbose feedback
428+
:type verbose: bool
399429
:return requests.Response or string
400430
401431
"""
402432
vapid_headers = None
403433
if vapid_claims:
434+
if verbose:
435+
print("Generating VAPID headers...")
404436
if not vapid_claims.get('aud'):
405437
url = urlparse(subscription_info.get('endpoint'))
406438
aud = "{}://{}".format(url.scheme, url.netloc)
@@ -411,6 +443,9 @@ def webpush(subscription_info,
411443
or vapid_claims.get('exp') < int(time.time())):
412444
# encryption lives for 12 hours
413445
vapid_claims['exp'] = int(time.time()) + (12 * 60 * 60)
446+
if verbose:
447+
print("Setting VAPID expry to {}...".format(
448+
vapid_claims['exp']))
414449
if not vapid_private_key:
415450
raise WebPushException("VAPID dict missing 'private_key'")
416451
if isinstance(vapid_private_key, Vapid):
@@ -422,8 +457,13 @@ def webpush(subscription_info,
422457
private_key_file=vapid_private_key) # pragma no cover
423458
else:
424459
vv = Vapid.from_string(private_key=vapid_private_key)
460+
if verbose:
461+
print("\t claims: {}".format(vapid_claims))
425462
vapid_headers = vv.sign(vapid_claims)
426-
response = WebPusher(subscription_info).send(
463+
if verbose:
464+
print("\t headers: {}".format(vapid_headers))
465+
466+
response = WebPusher(subscription_info, verbose=verbose).send(
427467
data,
428468
vapid_headers,
429469
ttl=ttl,
@@ -432,7 +472,7 @@ def webpush(subscription_info,
432472
timeout=timeout,
433473
)
434474
if not curl and response.status_code > 202:
435-
raise WebPushException("Push failed: {} {}".format(
436-
response.status_code, response.reason),
475+
raise WebPushException("Push failed: {} {}\nResponse body:{}".format(
476+
response.status_code, response.reason, response.text),
437477
response=response)
438478
return response

pywebpush/__main__.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ def get_config():
1515
parser.add_argument("--curl", help="Don't send, display as curl command",
1616
default=False, action="store_true")
1717
parser.add_argument("--encoding", default="aes128gcm")
18+
parser.add_argument("--verbose", "-v", help="Provide verbose feedback",
19+
default=False, action="store_true")
1820

1921
args = parser.parse_args()
2022

@@ -53,7 +55,8 @@ def main():
5355
vapid_private_key=args.key,
5456
vapid_claims=args.claims,
5557
curl=args.curl,
56-
content_encoding=args.encoding)
58+
content_encoding=args.encoding,
59+
verbose=args.verbose)
5760
print(result)
5861
except Exception as ex:
5962
print("ERROR: {}".format(ex))

pywebpush/tests/test_webpush.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -169,9 +169,8 @@ def repad(str):
169169
).decode('utf8')
170170
)
171171
ok_(subscription_info.get('endpoint').startswith(auth['aud']))
172-
ok_('WebPush' in pheaders.get('authorization'))
172+
ok_('vapid' in pheaders.get('authorization'))
173173
ckey = pheaders.get('crypto-key')
174-
ok_('p256ecdsa=' in ckey)
175174
ok_('dh=' in ckey)
176175
eq_(pheaders.get('content-encoding'), 'aesgcm')
177176

@@ -303,13 +302,12 @@ def test_as_curl(self, opener):
303302
)
304303
for s in [
305304
"curl -vX POST https://example.com",
306-
"-H \"crypto-key: p256ecdsa=",
307305
"-H \"content-encoding: aes128gcm\"",
308-
"-H \"authorization: WebPush ",
306+
"-H \"authorization: vapid ",
309307
"-H \"ttl: 0\"",
310308
"-H \"content-length:"
311309
]:
312-
ok_(s in result)
310+
ok_(s in result, "missing: {}".format(s))
313311

314312
def test_ci_dict(self):
315313
ci = CaseInsensitiveDict({"Foo": "apple", "bar": "banana"})

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from setuptools import find_packages, setup
55

66

7-
__version__ = "1.9.4"
7+
__version__ = "1.10.0"
88

99

1010
def read_from(file):

0 commit comments

Comments
 (0)