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
|
||||
|
||||
> WIP
|
||||
Migrated to https://git.startinblox.com/djangoldp-packages/djangoldp-webpushnotifications.
|
|
@ -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"})
|
Reference in New Issue