| @@ -1,9 +1,14 @@ | |||||
| PYTHON=python3 | PYTHON=python3 | ||||
| ALLFILES=templates/S2_perm_sw.html templates/S4_server.py venv static/i-ico.png static/i-banner.png | |||||
| .PHONY: run | .PHONY: run | ||||
| run: templates/S2_perm_sw.html templates/S4_server.py venv static/i-ico.png static/i-banner.png | |||||
| run: $(ALLFILES) | |||||
| ($(VENVACT) && python templates/S4_server.py $(PORT) ) | ($(VENVACT) && python templates/S4_server.py $(PORT) ) | ||||
| .PHONY: files | |||||
| files: $(ALLFILES) | |||||
| VENVACT=. ./venv/bin/activate | VENVACT=. ./venv/bin/activate | ||||
| venv: | venv: | ||||
| @@ -1,29 +1,123 @@ | |||||
| Web Push Notifications (WPN) | Web Push Notifications (WPN) | ||||
| =========================== | =========================== | ||||
| This is designed to be a super simple and small tool to push | |||||
| notifications to your end points. | |||||
| This is designed to be a super simple (understandable) and small tool | |||||
| to push notifications to web browsers (e.g. your cell phone). | |||||
| 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. | |||||
| The web browser Push API is fully encrypted and authenticated. This | |||||
| means that only your server and the client will be able to see the | |||||
| contents of the message. | |||||
| There is also a limit of 4k for the entire message that is composed | |||||
| and sent to the server. This means that the usable space for your | |||||
| own contents is around 3k, which is more than enough. | |||||
| Installation and Setup | |||||
| ---------------------- | |||||
| There are two ways to install it, one is to use it via the Flask web | |||||
| server, or via a CGI. | |||||
| ### Flask web server | |||||
| Installation/running: | |||||
| ``` | ``` | ||||
| git clonse <url> | |||||
| git clone https://www.funkthat.com/gitea/jmg/wpn | |||||
| cd wpn | cd wpn | ||||
| make run EMAIL=myemail@example.com PORT=<someport> | make run EMAIL=myemail@example.com PORT=<someport> | ||||
| ``` | ``` | ||||
| This code is partly copied from: | |||||
| https://gist.github.com/code-boxx/bc6aed37345ad1783cfb7d230f438120 | |||||
| Then you go to `http://127.0.0.1:<someport>/`, click the `Request | |||||
| Notifications` button. The sub info will be printed to the console, | |||||
| and a second later, a notification should appear, and it'll be repeated | |||||
| every 10 seconds. To change this behavior, look at the `donotify` | |||||
| fucntion in `src/S4_server.py`. | |||||
| But put into a repo w/ better install instructions, and turned into a | |||||
| usable bit of code. | |||||
| ### CGI | |||||
| First build the files needed: | |||||
| ``` | |||||
| git clone https://www.funkthat.com/gitea/jmg/wpn | |||||
| cd wpn | |||||
| make files EMAIL=myemail@example.com | |||||
| ``` | |||||
| Then copy the files in the directory `static`, the files | |||||
| `template/S2_perm_sw.html` and `push.py` to a directory on your | |||||
| webserver. In that directory, set push.py executable | |||||
| (`chmod 755 push.py`), and create a symlink to it from `push`: | |||||
| `ln -s push.py push`. | |||||
| Configure your web server such that the `push` is a CGI, and that | |||||
| `S2_perm_sw.html` is the index file. This is how to do it via Apache: | |||||
| ``` | |||||
| <Directory "/location/on/webserver"> | |||||
| Options +ExecCGI | |||||
| Order allow,deny | |||||
| Allow from all | |||||
| DirectoryIndex S2_perm_sw.html | |||||
| <FilesMatch "push$"> | |||||
| SetHandler cgi-script | |||||
| </FilesMatch> | |||||
| </Directory> | |||||
| ``` | |||||
| The `push.py` file is used to get the push notification subscription | |||||
| information from the browser and put it in `/tmp/subinfo.txt`. | |||||
| Note: The `push.py` has a hand implemented version of form decoding | |||||
| because Python had deprecated the `cgi` module. | |||||
| Pushing Notifications | |||||
| --------------------- | |||||
| Using the subscription information from the previous step, put it | |||||
| in a file for use as `<filewsubjson>` below. | |||||
| The `pywebpush` program that is installed by the `pywebpush` module | |||||
| is used to send notifications: | |||||
| ``` | |||||
| jq --arg email myemail@example.com -n '{ "sub": ("mailto:" + $email) }' > claim.txt | |||||
| jq --arg msg 'somemessage' --arg title notification -n '{ "title": $title, "body": $msg, "icon" : "static/i-ico.png", "image" : "static/i-banner.png" }' | | |||||
| pywebpush --data /dev/stdin --info <filewsubjson> --key keys/private_key.pem --claims claim.txt | |||||
| ``` | |||||
| As Python's `[venv](https://docs.python.org/3/library/venv.html#module-venv)` | |||||
| is used, you can simply execute/link to the pywebpush program in `venv/bin/` | |||||
| and not have to source the environment each time you need to run the program. | |||||
| Compatibility | |||||
| ------------- | |||||
| Currently Firefox for Android does not allow Push notifications. The | |||||
| nightly version does allow it. | |||||
| ### cryptography | |||||
| If you have troubles installing the `cryptography` dependency (due to | |||||
| rust or other compile issues), but can install `pycryptodome`, you can | |||||
| use the [pycryptowrap](https://www.funkthat.com/gitea/jmg/pycryptowrap) | |||||
| instead. To use it: | |||||
| ``` | |||||
| python3 -m venv venv | |||||
| (. ./venv/bin/activate && pip install git+https://www.funkthat.com/gitea/jmg/pycryptowrap ) | |||||
| (. ./venv/bin/activate && pip install flask ecdsa pywebpush ) | |||||
| ``` | |||||
| Notes | Notes | ||||
| ===== | |||||
| ----- | |||||
| If put behind a reverse proxy, make sure the url contains a trailing | 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 | slash. If you want the path w/o the slash to work, add a redirect as | ||||
| well. | |||||
| well. Though if you use the CGI version, this is likely unneeded. | |||||
| For Apache: | For Apache: | ||||
| ``` | ``` | ||||
| @@ -32,5 +126,11 @@ LoadModule proxy_http_module libexec/apache22/mod_proxy_http.so | |||||
| ProxyPass "/wpn/" "http://internalwpn.example.com/" | ProxyPass "/wpn/" "http://internalwpn.example.com/" | ||||
| ProxyPassReverse "/wpn/" "http://internalwpn.example.com/" | ProxyPassReverse "/wpn/" "http://internalwpn.example.com/" | ||||
| Redirect permanent /spn https://www.example.com/wpn/ | |||||
| Redirect permanent /wpn https://www.example.com/wpn/ | |||||
| ``` | ``` | ||||
| See the introduction section on message limits. | |||||
| Yes, I probably should have used a better template language than `sed`, | |||||
| but this is simple and works. It also makes it easy to copy over to a | |||||
| static site more easily. | |||||
| @@ -0,0 +1,146 @@ | |||||
| #!/usr/local/bin/python3 | |||||
| import email.mime.multipart | |||||
| import os | |||||
| import sys | |||||
| import traceback | |||||
| import unittest | |||||
| from email.message import Message | |||||
| from urllib.parse import parse_qs | |||||
| def _deb(*args): | |||||
| if True: | |||||
| print(*args, file=sys.stderr) | |||||
| def getdata(ct, bstr): | |||||
| _deb('ct:', repr(ct)) | |||||
| _deb('bstr:', repr(bstr)) | |||||
| m = Message() | |||||
| m['content-type'] = ct | |||||
| boundary = '--' + m.get_param('boundary') | |||||
| # trim the end: | |||||
| _deb('bstr:', repr(bstr)) | |||||
| # pretend we had the previous record | |||||
| body = b'\r\n' + bstr.split(('\r\n' + boundary + '--\r\n').encode('ASCII'), 1)[0] | |||||
| _deb('body:', repr(body)) | |||||
| parts = body.split(('\r\n' + boundary + '\r\n').encode('ASCII'))[1:] | |||||
| msgs = [ x for x in map(email.message_from_bytes, parts) if x.get_param('name', header='content-disposition') == 'sub' ] | |||||
| return msgs[0].get_payload() | |||||
| if __name__ == '__main__': | |||||
| _deb('env:', repr(os.environ)) | |||||
| contentlen = int(os.environ.get('CONTENT_LENGTH', '0')) | |||||
| body = sys.stdin.buffer.read(contentlen) | |||||
| try: | |||||
| sub = getdata(os.environ.get('CONTENT_TYPE'), body) | |||||
| os.umask(0o66) | |||||
| with open('/tmp/subinfo.txt', 'w') as fp: | |||||
| print(sub, file=fp) | |||||
| print('Content-Type: text/plain\r') | |||||
| print('\r') | |||||
| print('OK\r') | |||||
| except: | |||||
| _deb(traceback.format_exc()) | |||||
| print('status: 500 Server Error\r') | |||||
| print('Content-Type: text/plain\r') | |||||
| print('\r') | |||||
| print('ERROR\r') | |||||
| class Test(unittest.TestCase): | |||||
| _testdata = ''' | |||||
| 0x0040: 504f 5354 202f 7075 7368 2048 5454 ..POST./push.HTT | |||||
| 0x0050: 502f 312e 310d 0a48 6f73 743a 2031 3932 P/1.1..Host:.192 | |||||
| 0x0060: 2e31 3638 2e30 2e33 0d0a 5573 6572 2d41 .168.0.3..User-A | |||||
| 0x0070: 6765 6e74 3a20 4d6f 7a69 6c6c 612f 352e gent:.Mozilla/5. | |||||
| 0x0080: 3020 284d 6163 696e 746f 7368 3b20 496e 0.(Macintosh;.In | |||||
| 0x0090: 7465 6c20 4d61 6320 4f53 2058 2031 302e tel.Mac.OS.X.10. | |||||
| 0x00a0: 3135 3b20 7276 3a31 3039 2e30 2920 4765 15;.rv:109.0).Ge | |||||
| 0x00b0: 636b 6f2f 3230 3130 3031 3031 2046 6972 cko/20100101.Fir | |||||
| 0x00c0: 6566 6f78 2f31 3135 2e30 0d0a 4163 6365 efox/115.0..Acce | |||||
| 0x00d0: 7074 3a20 2a2f 2a0d 0a41 6363 6570 742d pt:.*/*..Accept- | |||||
| 0x00e0: 4c61 6e67 7561 6765 3a20 656e 2d55 532c Language:.en-US, | |||||
| 0x00f0: 656e 3b71 3d30 2e35 0d0a 4163 6365 7074 en;q=0.5..Accept | |||||
| 0x0100: 2d45 6e63 6f64 696e 673a 2067 7a69 702c -Encoding:.gzip, | |||||
| 0x0110: 2064 6566 6c61 7465 2c20 6272 0d0a 5265 .deflate,.br..Re | |||||
| 0x0120: 6665 7265 723a 2068 7474 7073 3a2f 2f77 ferer:.https://w | |||||
| 0x0130: 7777 2e66 756e 6b74 6861 742e 636f 6d2f ww.funkthat.com/ | |||||
| 0x0140: 7765 6270 7573 682f 0d0a 436f 6e74 656e webpush/..Conten | |||||
| 0x0150: 742d 5479 7065 3a20 6d75 6c74 6970 6172 t-Type:.multipar | |||||
| 0x0160: 742f 666f 726d 2d64 6174 613b 2062 6f75 t/form-data;.bou | |||||
| 0x0170: 6e64 6172 793d 2d2d 2d2d 2d2d 2d2d 2d2d ndary=---------- | |||||
| 0x0180: 2d2d 2d2d 2d2d 2d2d 2d2d 2d2d 2d2d 2d2d ---------------- | |||||
| 0x0190: 2d33 3435 3837 3235 3136 3133 3033 3132 -345872516130312 | |||||
| 0x01a0: 3635 3935 3233 3135 3033 3739 3130 310d 659523150379101. | |||||
| 0x01b0: 0a4f 7269 6769 6e3a 2068 7474 7073 3a2f .Origin:.https:/ | |||||
| 0x01c0: 2f77 7777 2e66 756e 6b74 6861 742e 636f /www.funkthat.co | |||||
| 0x01d0: 6d0d 0a44 4e54 3a20 310d 0a53 6563 2d46 m..DNT:.1..Sec-F | |||||
| 0x01e0: 6574 6368 2d44 6573 743a 2065 6d70 7479 etch-Dest:.empty | |||||
| 0x01f0: 0d0a 5365 632d 4665 7463 682d 4d6f 6465 ..Sec-Fetch-Mode | |||||
| 0x0200: 3a20 636f 7273 0d0a 5365 632d 4665 7463 :.cors..Sec-Fetc | |||||
| 0x0210: 682d 5369 7465 3a20 7361 6d65 2d6f 7269 h-Site:.same-ori | |||||
| 0x0220: 6769 6e0d 0a53 6563 2d47 5043 3a20 310d gin..Sec-GPC:.1. | |||||
| 0x0230: 0a58 2d46 6f72 7761 7264 6564 2d46 6f72 .X-Forwarded-For | |||||
| 0x0240: 3a20 3139 322e 3136 382e 302e 330d 0a58 :.192.168.0.3..X | |||||
| 0x0250: 2d46 6f72 7761 7264 6564 2d48 6f73 743a -Forwarded-Host: | |||||
| 0x0260: 2077 7777 2e66 756e 6b74 6861 742e 636f .www.funkthat.co | |||||
| 0x0270: 6d0d 0a58 2d46 6f72 7761 7264 6564 2d53 m..X-Forwarded-S | |||||
| 0x0280: 6572 7665 723a 2077 7777 2e66 756e 6b74 erver:.www.funkt | |||||
| 0x0290: 6861 742e 636f 6d0d 0a43 6f6e 6e65 6374 hat.com..Connect | |||||
| 0x02a0: 696f 6e3a 204b 6565 702d 416c 6976 650d ion:.Keep-Alive. | |||||
| 0x02b0: 0a43 6f6e 7465 6e74 2d4c 656e 6774 683a .Content-Length: | |||||
| 0x02c0: 2035 3833 0d0a 0d0a 2d2d 2d2d 2d2d 2d2d .583....-------- | |||||
| 0x02d0: 2d2d 2d2d 2d2d 2d2d 2d2d 2d2d 2d2d 2d2d ---------------- | |||||
| 0x02e0: 2d2d 2d2d 2d33 3435 3837 3235 3136 3133 -----34587251613 | |||||
| 0x02f0: 3033 3132 3635 3935 3233 3135 3033 3739 0312659523150379 | |||||
| 0x0300: 3130 310d 0a43 6f6e 7465 6e74 2d44 6973 101..Content-Dis | |||||
| 0x0310: 706f 7369 7469 6f6e 3a20 666f 726d 2d64 position:.form-d | |||||
| 0x0320: 6174 613b 206e 616d 653d 2273 7562 220d ata;.name="sub". | |||||
| 0x0330: 0a0d 0a7b 2265 6e64 706f 696e 7422 3a22 ...{"endpoint":" | |||||
| 0x0340: 6874 7470 733a 2f2f 7570 6461 7465 732e https://updates. | |||||
| 0x0350: 7075 7368 2e73 6572 7669 6365 732e 6d6f push.services.mo | |||||
| 0x0360: 7a69 6c6c 612e 636f 6d2f 7770 7573 682f zilla.com/wpush/ | |||||
| 0x04c0: 5a4e 565a 6441 535f 4954 3822 7d7d 0d0a ZNVZdAS_IT8"}}.. | |||||
| 0x04d0: 2d2d 2d2d 2d2d 2d2d 2d2d 2d2d 2d2d 2d2d ---------------- | |||||
| 0x04e0: 2d2d 2d2d 2d2d 2d2d 2d2d 2d2d 2d33 3435 -------------345 | |||||
| 0x04f0: 3837 3235 3136 3133 3033 3132 3635 3935 8725161303126595 | |||||
| 0x0500: 3233 3135 3033 3739 3130 312d 2d0d 0a 23150379101--.. | |||||
| ''' | |||||
| @staticmethod | |||||
| def _process_hexdump(data): | |||||
| lines = (x.split(':', 1)[1].strip().split(' ', 1)[0] for x in data.split('\n') if x.strip()) | |||||
| return bytes.fromhex(''.join(lines)) | |||||
| def test_basic(self): | |||||
| bstr = self._process_hexdump(self._testdata) | |||||
| # drop post line | |||||
| bstr = bstr.split(b'\r\n', 1)[1] | |||||
| _deb(repr(bstr)) | |||||
| msg = email.message_from_bytes(bstr) | |||||
| _deb('msg hdrs:', repr(msg.items())) | |||||
| # get ct | |||||
| ct = msg['content-type'] | |||||
| _deb('ct:', repr(ct)) | |||||
| # get body: | |||||
| body = bstr.split(b'\r\n\r\n', 1)[1] | |||||
| # do the actual test: | |||||
| res = getdata(ct, body) | |||||
| self.assertEqual(res, '{"endpoint":"https://updates.push.services.mozilla.com/wpush/ZNVZdAS_IT8"}}') | |||||