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