@@ -0,0 +1,6 @@ | |||||
# Python venv | |||||
venv | |||||
# Generated | |||||
keys | |||||
templates |
@@ -0,0 +1,47 @@ | |||||
See: https://gist.github.com/code-boxx/bc6aed37345ad1783cfb7d230f438120 | |||||
Last modified Nov 7th, 2023 | |||||
Copyright by Code Boxx | |||||
Permission is hereby granted, free of charge, to any person obtaining | |||||
a copy of this software and associated documentation files (the | |||||
"Software"), to deal in the Software without restriction, including | |||||
without limitation the rights to use, copy, modify, merge, publish, | |||||
distribute, sublicense, and/or sell copies of the Software, and to | |||||
permit persons to whom the Software is furnished to do so, subject to | |||||
the following conditions: | |||||
The above copyright notice and this permission notice shall be included | |||||
in all copies or substantial portions of the Software. | |||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS | |||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF | |||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. | |||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY | |||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, | |||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE | |||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | |||||
New code: | |||||
Copyright 2023 John-Mark Gurney. | |||||
Redistribution and use in source and binary forms, with or without | |||||
modification, are permitted provided that the following conditions | |||||
are met: | |||||
1. Redistributions of source code must retain the above copyright | |||||
notice, this list of conditions and the following disclaimer. | |||||
2. Redistributions in binary form must reproduce the above copyright | |||||
notice, this list of conditions and the following disclaimer in the | |||||
documentation and/or other materials provided with the distribution. | |||||
THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND | |||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | |||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE | |||||
ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE | |||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL | |||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS | |||||
OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) | |||||
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT | |||||
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY | |||||
OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF | |||||
SUCH DAMAGE. |
@@ -0,0 +1,28 @@ | |||||
PYTHON=python3 | |||||
.PHONY: run | |||||
run: templates/S2_perm_sw.html templates/S4_server.py venv static/i-ico.png | |||||
($(VENVACT) && python templates/S4_server.py ) | |||||
VENVACT=. ./venv/bin/activate | |||||
venv: | |||||
$(PYTHON) -m venv venv && ( $(VENVACT) && pip install flask ecdsa pywebpush) || rm -rf venv | |||||
# python S1_vapid.py | |||||
templates/S2_perm_sw.html: src/S2_perm_sw.html keys/public_key.txt | |||||
mkdir -p templates | |||||
sed -e 's/YOUR-PUBLIC-KEY/'"$$(cat keys/public_key.txt)"'/' < src/S2_perm_sw.html > templates/S2_perm_sw.html | |||||
templates/S4_server.py: src/S4_server.py keys/private_key.txt | |||||
if [ -z "$(EMAIL)" ]; then echo Must specify EMAIL.; exit 1; fi | |||||
mkdir -p templates | |||||
sed -e 's/your@email.com/$(EMAIL)/' -e 's/YOUR-PRIVATE-KEY/'"$$(cat keys/private_key.txt)"'/' < src/S4_server.py > templates/S4_server.py | |||||
# XXX - HOST_* and VAPID_SUBJECT | |||||
keys/public_key.txt keys/private_key.txt: venv S1_vapid.py | |||||
( $(VENVACT) && python S1_vapid.py ) | |||||
static/i-ico.png: | |||||
echo P6 1 1 255 255 0 0 | pnmtopng > $@ |
@@ -0,0 +1,36 @@ | |||||
Web Push Notifications (WPN) | |||||
=========================== | |||||
This is designed to be a super simple and small tool to push | |||||
notifications to your end points. | |||||
Installation/running: | |||||
``` | |||||
git clonse <url> | |||||
cd wpn | |||||
make run EMAIL=myemail@example.com PORT=<someport> | |||||
``` | |||||
This code is partly copied from: | |||||
https://gist.github.com/code-boxx/bc6aed37345ad1783cfb7d230f438120 | |||||
But put into a repo w/ better install instructions, and turned into a | |||||
usable bit of code. | |||||
Notes | |||||
===== | |||||
If put behind a reverse proxy, make sure the url contains a trailing | |||||
slash. If you want the path w/o the slash to work, add a redirect as | |||||
well. | |||||
For Apache: | |||||
``` | |||||
LoadModule proxy_module libexec/apache22/mod_proxy.so | |||||
LoadModule proxy_http_module libexec/apache22/mod_proxy_http.so | |||||
ProxyPass "/wpn/" "http://internalwpn.example.com/" | |||||
ProxyPassReverse "/wpn/" "http://internalwpn.example.com/" | |||||
Redirect permanent /spn https://www.example.com/wpn/ | |||||
``` |
@@ -0,0 +1,21 @@ | |||||
# (A) REQUIRED MODULES | |||||
import base64 | |||||
import ecdsa | |||||
# (B) GENERATE KEYS | |||||
# CREDITS : https://gist.github.com/cjies/cc014d55976db80f610cd94ccb2ab21e | |||||
pri = ecdsa.SigningKey.generate(curve=ecdsa.NIST256p) | |||||
pub = pri.get_verifying_key() | |||||
private = base64.urlsafe_b64encode(pri.to_string()).decode("utf-8").strip("="), | |||||
public = base64.urlsafe_b64encode(b"\x04" + pub.to_string()).decode("utf-8").strip("=") | |||||
import pathlib | |||||
keydir = pathlib.Path('keys') | |||||
keydir.mkdir(exist_ok=True) | |||||
with open(keydir / 'public_key.txt', 'w') as fp: | |||||
print(public, file=fp) | |||||
with open(keydir / 'private_key.txt', 'w') as fp: | |||||
print(private, file=fp) |
@@ -0,0 +1,82 @@ | |||||
<!DOCTYPE html> | |||||
<html> | |||||
<head> | |||||
<title>Push Notification</title> | |||||
<meta charset="utf-8"> | |||||
</head> | |||||
<body> | |||||
<div id="allow-push-notification-bar" class="allow-push-notification-bar"> | |||||
<div class="buttons-more"> | |||||
<button type="button" class="ok-button button-1" id="allow-push-notification" | |||||
onclick="requestnotifyperm();"> | |||||
Request Notifications | |||||
</button> | |||||
</div> | |||||
</div> | |||||
<div id="notif-allowed" style="display: none">Notifications allowed</div> | |||||
<script> | |||||
function notifyallowed() { | |||||
document.getElementById("notif-allowed").style.display = 'block'; | |||||
document.getElementById("allow-push-notification-bar").style.display = 'none'; | |||||
document.getElementById("allow-push-notification").style.display = 'none'; | |||||
} | |||||
// (A) OBTAIN USER PERMISSION TO SHOW NOTIFICATION | |||||
function requestnotifyperm() { | |||||
// (A1) ASK FOR PERMISSION | |||||
if (Notification.permission === "default") { | |||||
Notification.requestPermission().then(perm => { | |||||
if (Notification.permission === "granted") { | |||||
notifyallowed() | |||||
regWorker().catch(err => console.error(err)); | |||||
} else { | |||||
alert("Please allow notifications."); | |||||
} | |||||
}); | |||||
} | |||||
} | |||||
if (Notification.permission === "granted") { | |||||
notifyallowed() | |||||
regWorker().catch(err => console.error(err)); | |||||
} | |||||
// (B) REGISTER SERVICE WORKER | |||||
async function regWorker () { | |||||
// (B1) YOUR PUBLIC KEY - CHANGE TO YOUR OWN! | |||||
const publicKey = "YOUR-PUBLIC-KEY"; | |||||
console.log("registering..."); | |||||
// (B2) REGISTER SERVICE WORKER | |||||
// broken on firefox for android | |||||
//navigator.serviceWorker.register("/webpush/S3_sw.js"); | |||||
navigator.serviceWorker.register("S3_sw.js"); | |||||
// (B3) SUBSCRIBE TO PUSH SERVER | |||||
navigator.serviceWorker.ready | |||||
.then(reg => { | |||||
console.log("registered..."); | |||||
reg.pushManager.subscribe({ | |||||
userVisibleOnly: true, | |||||
applicationServerKey: publicKey | |||||
}).then( | |||||
// (B3-1) OK - TEST PUSH NOTIFICATION | |||||
sub => { | |||||
console.log("pushing..."); | |||||
var data = new FormData(); | |||||
data.append("sub", JSON.stringify(sub)); | |||||
fetch("push", { method:"POST", body:data }) | |||||
.then(res => res.text()) | |||||
.then(txt => console.log(txt)) | |||||
.catch(err => console.error(err)); | |||||
}, | |||||
// (B3-2) ERROR! | |||||
err => console.error(err) | |||||
); | |||||
}); | |||||
} | |||||
</script> | |||||
</body> | |||||
</html> |
@@ -0,0 +1,83 @@ | |||||
# (A) INIT | |||||
# (A1) LOAD MODULES | |||||
from flask import Flask, render_template, request, make_response, send_from_directory | |||||
from pywebpush import webpush, WebPushException | |||||
import json | |||||
import time | |||||
import threading | |||||
# (A2) FLASK SETTINGS + INIT - CHANGE TO YOUR OWN! | |||||
HOST_NAME = "localhost" | |||||
HOST_NAME = "0.0.0.0" | |||||
HOST_PORT = 80 | |||||
VAPID_SUBJECT = "mailto:your@email.com" | |||||
VAPID_PRIVATE = "YOUR-PRIVATE-KEY" | |||||
app = Flask(__name__, | |||||
static_folder='../static', | |||||
template_folder='.', | |||||
) | |||||
app.debug = True | |||||
#app.config['EXPLAIN_TEMPLATE_LOADING'] = True | |||||
# (B) VIEWS | |||||
# (B1) "LANDING PAGE" | |||||
@app.route("/") | |||||
def index(): | |||||
return render_template("S2_perm_sw.html") | |||||
# (B2) SERVICE WORKER | |||||
@app.route("/S3_sw.js") | |||||
def sw(): | |||||
import sys | |||||
print(repr(app.static_folder), file=sys.stderr) | |||||
response = make_response(send_from_directory(app.static_folder, "S3_sw.js")) | |||||
return response | |||||
threadlist = [] | |||||
def donotify(sub, sleep=10): | |||||
global threadlist | |||||
for t in threadlist: | |||||
t.join(0) | |||||
threadlist = [ t for t in threadlist if t.is_alive() ] | |||||
if sleep: | |||||
time.sleep(sleep) | |||||
print('sending notification...') | |||||
try: | |||||
webpush( | |||||
subscription_info = sub, | |||||
data = json.dumps({ | |||||
"title" : "Welcome!", | |||||
"body" : "Yes, it works!", | |||||
"icon" : "static/i-ico.png", | |||||
"image" : "static/i-banner.png" | |||||
}), | |||||
vapid_private_key = VAPID_PRIVATE, | |||||
vapid_claims = { "sub": VAPID_SUBJECT } | |||||
) | |||||
except WebPushException as ex: | |||||
print(ex) | |||||
t = threading.Thread(target=donotify, args=(sub,)) | |||||
t.run() | |||||
threadlist.append(t) | |||||
# (B3) PUSH DEMO | |||||
@app.route("/push", methods=["POST"]) | |||||
def push(): | |||||
# (B3-1) GET SUBSCRIBER | |||||
sub = json.loads(request.form["sub"]) | |||||
import sys | |||||
print('sub:', repr(sub), file=sys.stderr) | |||||
# (B3-2) TEST PUSH NOTIFICATION | |||||
result = "OK" | |||||
donotify(sub, 1) | |||||
return result | |||||
# (C) START | |||||
if __name__ == "__main__": | |||||
app.run(HOST_NAME, HOST_PORT) |
@@ -0,0 +1,31 @@ | |||||
// (A) INSTANT WORKER ACTIVATION | |||||
self.addEventListener("install", evt => self.skipWaiting()); | |||||
// (B) CLAIM CONTROL INSTANTLY | |||||
self.addEventListener("activate", evt => self.clients.claim()); | |||||
// (C) LISTEN TO PUSH | |||||
self.addEventListener("push", evt => { | |||||
const data = evt.data.json(); | |||||
console.log("got: " + evt.data); | |||||
self.registration.showNotification(data.title, { | |||||
body: data.body, | |||||
icon: data.icon, | |||||
image: data.image | |||||
}); | |||||
}); | |||||
// (D) HANDLE USER INTERACTION | |||||
// https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerGlobalScope/notificationclick_event | |||||
self.addEventListener( "notificationclick", (event) => { | |||||
event.notification.close(); | |||||
if (event.action === "archive") { | |||||
// User selected the Archive action. | |||||
//archiveEmail(); | |||||
} else { | |||||
// User selected (e.g., clicked in) the main body of notification. | |||||
//clients.openWindow("/inbox"); | |||||
} | |||||
}, | |||||
false, | |||||
); |