34 Commits

Author SHA1 Message Date
46f95e405f Add migration notice 2021-04-21 15:56:20 +02:00
3wc
9caf97966c Load django-webpush VAPID keys from DB 2021-04-15 18:57:22 +02:00
3wc
0fdbd1f3b2 Fix TypeError rendering VAPIDKeyset.public_key 2021-04-15 18:57:05 +02:00
c64d1029b4 Add failing test for missing VAPID key 2021-04-15 15:06:37 +02:00
a0c0aa760d Merge imports 2021-04-15 14:28:34 +02:00
c433a7d561 Fixup subscription test to use a VAPIDKeyset 2021-04-15 14:28:20 +02:00
59ce5900e0 Appease formatter 2021-04-15 14:27:58 +02:00
63c7384e50 Don't call tobytes on bytes 2021-04-15 14:27:44 +02:00
aaf365dba2 Add webpush model checks into sub test
Let the formatter have its way also.
2021-04-15 09:42:16 +02:00
3wc
7ee6ac7691 Make TestAcceptSubscription work..
..by using Django default test client instead of the one from
`rest_framework`.
2021-04-14 21:34:31 +02:00
fbb58b1737 Some WIP progress on writing webpush tests 2021-04-08 16:59:00 +02:00
6542993bf0 Fix name of test 2021-04-08 16:29:40 +02:00
74b2712699 Test VAPIDKeyset can generate a public key 2021-04-08 16:28:49 +02:00
3wc
d03672e01e Don't try and reference views.home 2021-04-08 15:37:23 +02:00
2760e70c8e remove js template 2021-04-08 14:20:14 +01:00
5c13b626a8 Resolve tabbing/indentation issues 2021-04-07 17:18:09 +02:00
44153bc562 Re-work access to pub/priv keys for showing 2021-04-07 17:14:16 +02:00
3wc
64015d9b5b Remove VAPIDKeyset.public_key, generate on demand
Also rename `gen_vapid_keys` to `gen_vapid_key`, and merge migrations
2021-04-07 17:07:21 +02:00
6ac2905a6c Make note of core 2021-04-07 16:53:58 +02:00
1843946342 Fix wave of typos 2021-04-07 16:53:24 +02:00
91fbb3dde1 Add boilerplate for unit testing 2021-04-07 16:51:55 +02:00
2fb474c87c Throw down some docs in that README 2021-04-07 16:42:45 +02:00
3wc
c05a8e65dc Fix gen_vapid_keys 2021-04-07 16:26:37 +02:00
89680cf6f1 Drop boilerplate 2021-04-07 16:21:29 +02:00
3wc
2f6a345e5c Fix admin.py 2021-04-07 16:19:50 +02:00
4aa1d000b9 Merge branch 'master' of https://git.autonomic.zone/autonomic-cooperative/djangoldp-webpushnotification 2021-04-07 15:18:48 +01:00
bee22e5e5e add webpush as installed_app 2021-04-07 15:18:11 +01:00
d3492dc266 Add license file 2021-04-07 16:13:55 +02:00
4fd70faf99 merged changes hopefully not broooken 2021-04-07 10:11:30 -04:00
cf6c0ed1d0 moved files 2021-04-07 10:08:01 -04:00
b86872be24 Add django-webpush to deps 2021-04-07 15:57:33 +02:00
764e9aaa3f Initial boilerplate for accepting subscriptions 2021-04-07 15:56:09 +02:00
3wc
a8492491b9 Fix models.py 2021-04-07 15:52:32 +02:00
9d19e0e73c added in this stuff https://git.startinblox.com/djangoldp-packages/djangoldp-notification/merge_requests/43#note_59310 2021-04-07 09:44:56 -04:00
14 changed files with 288 additions and 4 deletions

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018 Startin blox
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.

View File

@ -1,3 +1 @@
# djangoldp-webpushnotification
> WIP
Migrated to https://git.startinblox.com/djangoldp-packages/djangoldp-webpushnotifications.

View File

@ -0,0 +1,19 @@
from django.contrib import admin
from djangoldp.admin import DjangoLDPAdmin
from .models import VAPIDKeyset
class VAPIDKeysetAdmin(DjangoLDPAdmin):
readonly_fields = ('public_key_view', 'private_key_view')
def public_key_view(self, obj):
return obj.public_key
def private_key_view(self, obj):
return obj.private_key.tobytes()
class Meta:
verbose_name = 'VAPID key-set'
admin.site.register(VAPIDKeyset, VAPIDKeysetAdmin)

View File

@ -0,0 +1,5 @@
"""This module is loaded by DjangoLDP core during setup."""
INSTALLED_APPS = [
'webpush'
]

View File

@ -0,0 +1,9 @@
from django.urls import include, path
from django.views.generic import TemplateView
from .views import send_push
urlpatterns = [
path("send_push", send_push),
path("webpush/", include("webpush.urls")),
]

View File

@ -0,0 +1,21 @@
from base64 import urlsafe_b64encode
import ecdsa
from django.core.management.base import BaseCommand
from djangoldp_webpushnotification.models import VAPIDKeyset
from ecdsa import SigningKey
class Command(BaseCommand):
help = "Generate VAPID key pair"
def handle(self, *args, **options):
priv_key = SigningKey.generate(curve=ecdsa.NIST256p)
VAPIDKeyset.objects.create(
private_key=urlsafe_b64encode(priv_key.to_string()).strip(b"=")
)
self.stdout.write("VAPID Keyset succesfully generated")
exit(0)

View File

@ -0,0 +1,21 @@
# Generated by Django 2.2.19 on 2021-04-07 14:38
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='VAPIDKeyset',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('private_key', models.BinaryField(max_length=43)),
],
),
]

View File

@ -0,0 +1,23 @@
from base64 import urlsafe_b64decode, urlsafe_b64encode
from django.db import models
from ecdsa import NIST256p, SigningKey
class VAPIDKeyset(models.Model):
private_key = models.BinaryField(max_length=43)
def __str__(self):
return "public_key:{}... private_key:{}...".format(
self.public_key[:10], self.private_key[:10]
)
@property
def public_key(self):
key_str = self.private_key.tobytes()
padding = len(key_str) % 4
key_str += b"=" * padding
key = SigningKey.from_string(
urlsafe_b64decode(key_str), curve=NIST256p
).get_verifying_key()
return urlsafe_b64encode(b"\x04" + key.to_string()).strip(b"=")

View File

@ -0,0 +1,23 @@
import sys
import django
import yaml
from django.conf import settings as django_settings
from djangoldp.conf.ldpsettings import LDPSettings
from djangoldp_webpushnotification.tests.settings_default import yaml_config
config = yaml.safe_load(yaml_config)
ldpsettings = LDPSettings(config)
django_settings.configure(ldpsettings)
django.setup()
from django.test.runner import DiscoverRunner
test_runner = DiscoverRunner(verbosity=1)
failures = test_runner.run_tests([
'djangoldp_webpushnotification.tests.tests_vapidkeyset',
'djangoldp_webpushnotification.tests.tests_accept_subscription',
])
if failures:
sys.exit(failures)

View File

@ -0,0 +1,10 @@
"""YAML configurations for djangoldp_webpushnotification testing."""
yaml_config = """
dependencies:
ldppackages:
- djangoldp_account
- djangoldp_webpushnotification
- djangoldp_webpushnotification.tests
"""

View File

@ -0,0 +1,78 @@
import json
from base64 import urlsafe_b64encode
from django.contrib.auth import get_user_model
from django.test import Client, TestCase
from django.urls import reverse
from djangoldp_webpushnotification.models import VAPIDKeyset
from ecdsa import NIST256p, SigningKey
from webpush.models import PushInformation, SubscriptionInfo
class TestAcceptSubscription(TestCase):
def setUp(self):
self.client = Client()
self.user = get_user_model().objects.create(
username="john", email="jlennon@beatles.com", password="glass onion"
)
self.client.force_login(self.user)
def tearDown(self):
self.user.delete()
def gen_vapid_key(self):
generated = SigningKey.generate(curve=NIST256p)
encoded = urlsafe_b64encode(generated.to_string()).strip(b"=")
return VAPIDKeyset.objects.create(private_key=encoded)
def test_accept_sub(self):
vapid_key_set = self.gen_vapid_key()
payload = {
"status_type": "subscribe",
"subscription": {
"endpoint": "https://hubl.example.com",
"keys": {
"auth": "front-end-generated-secret",
"p256dh": vapid_key_set.public_key.decode("utf-8"),
},
},
"browser": "firefox",
}
url = reverse("save_webpush_info")
response = self.client.post(
url, data=json.dumps(payload), content_type="application/json"
)
self.assertEqual(response.status_code, 201)
sub_info = SubscriptionInfo.objects.get()
self.assertEqual(sub_info.browser, "firefox")
self.assertEqual(sub_info.endpoint, "https://hubl.example.com")
self.assertEqual(sub_info.auth, "front-end-generated-secret")
self.assertEqual(sub_info.p256dh, vapid_key_set.public_key.decode("utf-8"))
push_info = PushInformation.objects.get()
self.assertEqual(push_info.user, self.user)
self.assertEqual(push_info.subscription, sub_info)
def test_accept_sub_missing_vapid_key(self):
payload = {
"status_type": "subscribe",
"subscription": {
"endpoint": "https://hubl.example.com",
"keys": {
"auth": "front-end-generated-secret",
"p256dh": "INVALID-PUBLIC-KEY",
},
},
"browser": "firefox",
}
url = reverse("save_webpush_info")
response = self.client.post(
url, data=json.dumps(payload), content_type="application/json"
)
self.assertEqual(response.status_code, 403)

View File

@ -0,0 +1,14 @@
from base64 import urlsafe_b64encode
from django.test import TestCase
from djangoldp_webpushnotification.models import VAPIDKeyset
from ecdsa import NIST256p, SigningKey
class TestVAPIDKeySet(TestCase):
def test_vapidkeyset_public_key(self):
priv_key = SigningKey.generate(curve=NIST256p)
vapid_key_set = VAPIDKeyset.objects.create(
private_key=urlsafe_b64encode(priv_key.to_string()).strip(b"=")
)
assert isinstance(vapid_key_set.public_key, bytes)

View File

@ -0,0 +1,38 @@
import json
from django.conf import settings
from django.http.response import HttpResponse, JsonResponse
from django.shortcuts import get_object_or_404, render
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_GET, require_POST
from webpush import send_user_notification
from djangoldp_account.models import LDPUser
from djangoldp_webpushnotification.models import VAPIDKeyset
@require_POST
@csrf_exempt
def send_push(request):
try:
body = request.body
data = json.loads(body)
if "head" not in data or "body" not in data or "id" not in data:
return JsonResponse(status=400, data={"message": "Invalid data format"})
user_id = data["id"]
user = get_object_or_404(LDPUser, pk=user_id)
payload = {"head": data["head"], "body": data["body"]}
vapid_key = VAPIDKeyset.objects.first()
settings.WEBPUSH_SETTINGS = {
'VAPID_PUBLIC_KEY': vapid_key.public_key,
'VAPID_PRIVATE_KEY': vapid_key.private_key.tobytes().decode(),
'VAPID_ADMIN_EMAIL': 'foo@bar.com',
}
send_user_notification(user=user, payload=payload, ttl=1000)
return JsonResponse(status=200, data={"message": "Web push successful"})
except TypeError:
return JsonResponse(status=500, data={"message": "An error occurred"})

View File

@ -10,9 +10,13 @@ license = MIT
[options]
packages = find:
install_requires =
djangoldp~=0.5
djangoldp~=2.1
djangoldp_account~=2.1
ecdsa~=0.16.1
django-webpush~=0.3
[options.extras_require]
include_package_data = True
dev =
factory_boy>=2.11.0