commit 5a565008b866080a80aeacb3f4179baf92a78c36 Author: John-Mark Gurney Date: Thu Nov 16 01:49:08 2023 -0800 inital work to make notifications work.. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..247790c --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +# Python venv +venv + +# Generated +keys +templates diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..c4fe6a7 --- /dev/null +++ b/LICENSE.txt @@ -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. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..5d0e0db --- /dev/null +++ b/Makefile @@ -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 > $@ diff --git a/README.md b/README.md new file mode 100644 index 0000000..1a64dec --- /dev/null +++ b/README.md @@ -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 +cd wpn +make run EMAIL=myemail@example.com PORT= +``` + +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/ +``` diff --git a/S1_vapid.py b/S1_vapid.py new file mode 100644 index 0000000..d66950e --- /dev/null +++ b/S1_vapid.py @@ -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) diff --git a/src/S2_perm_sw.html b/src/S2_perm_sw.html new file mode 100644 index 0000000..5b8c627 --- /dev/null +++ b/src/S2_perm_sw.html @@ -0,0 +1,82 @@ + + + + Push Notification + + + +
+
+ +
+
+ + + + diff --git a/src/S4_server.py b/src/S4_server.py new file mode 100644 index 0000000..9a9d33d --- /dev/null +++ b/src/S4_server.py @@ -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) diff --git a/static/S3_sw.js b/static/S3_sw.js new file mode 100644 index 0000000..975c14f --- /dev/null +++ b/static/S3_sw.js @@ -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, +);