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