@@ -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, | |||
); |