Compare commits
34 Commits
accept-sub
...
master
Author | SHA1 | Date |
---|---|---|
decentral1se | 46f95e405f | |
3wc | 9caf97966c | |
3wc | 0fdbd1f3b2 | |
decentral1se | c64d1029b4 | |
decentral1se | a0c0aa760d | |
decentral1se | c433a7d561 | |
decentral1se | 59ce5900e0 | |
decentral1se | 63c7384e50 | |
decentral1se | aaf365dba2 | |
3wc | 7ee6ac7691 | |
decentral1se | fbb58b1737 | |
decentral1se | 6542993bf0 | |
decentral1se | 74b2712699 | |
3wc | d03672e01e | |
Fay Arnold | 2760e70c8e | |
decentral1se | 5c13b626a8 | |
decentral1se | 44153bc562 | |
3wc | 64015d9b5b | |
decentral1se | 6ac2905a6c | |
decentral1se | 1843946342 | |
decentral1se | 91fbb3dde1 | |
decentral1se | 2fb474c87c | |
3wc | c05a8e65dc | |
decentral1se | 89680cf6f1 | |
3wc | 2f6a345e5c | |
Fay Arnold | 4aa1d000b9 | |
Fay Arnold | bee22e5e5e | |
decentral1se | d3492dc266 | |
trav | 4fd70faf99 | |
trav | cf6c0ed1d0 | |
decentral1se | b86872be24 | |
decentral1se | 764e9aaa3f | |
3wc | a8492491b9 | |
trav | 9d19e0e73c |
|
@ -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.
|
|
@ -1,3 +1 @@
|
||||||
# djangoldp-webpushnotification
|
Migrated to https://git.startinblox.com/djangoldp-packages/djangoldp-webpushnotifications.
|
||||||
|
|
||||||
> WIP
|
|
|
@ -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)
|
|
@ -0,0 +1,5 @@
|
||||||
|
"""This module is loaded by DjangoLDP core during setup."""
|
||||||
|
|
||||||
|
INSTALLED_APPS = [
|
||||||
|
'webpush'
|
||||||
|
]
|
|
@ -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")),
|
||||||
|
]
|
|
@ -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)
|
|
@ -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)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
|
@ -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"=")
|
|
@ -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)
|
|
@ -0,0 +1,10 @@
|
||||||
|
"""YAML configurations for djangoldp_webpushnotification testing."""
|
||||||
|
|
||||||
|
yaml_config = """
|
||||||
|
dependencies:
|
||||||
|
|
||||||
|
ldppackages:
|
||||||
|
- djangoldp_account
|
||||||
|
- djangoldp_webpushnotification
|
||||||
|
- djangoldp_webpushnotification.tests
|
||||||
|
"""
|
|
@ -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)
|
|
@ -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)
|
|
@ -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"})
|
|
@ -10,9 +10,13 @@ license = MIT
|
||||||
[options]
|
[options]
|
||||||
packages = find:
|
packages = find:
|
||||||
install_requires =
|
install_requires =
|
||||||
djangoldp~=0.5
|
djangoldp~=2.1
|
||||||
|
djangoldp_account~=2.1
|
||||||
|
ecdsa~=0.16.1
|
||||||
|
django-webpush~=0.3
|
||||||
|
|
||||||
[options.extras_require]
|
[options.extras_require]
|
||||||
|
include_package_data = True
|
||||||
dev =
|
dev =
|
||||||
factory_boy>=2.11.0
|
factory_boy>=2.11.0
|
||||||
|
|
||||||
|
|
Reference in New Issue