Initial version
This commit is contained in:
commit
419a958bb7
23 changed files with 473 additions and 0 deletions
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
__pycache__/
|
||||
*.pyc
|
||||
venv/
|
||||
db.sqlite3
|
0
frontend/__init__.py
Normal file
0
frontend/__init__.py
Normal file
3
frontend/admin.py
Normal file
3
frontend/admin.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
5
frontend/apps.py
Normal file
5
frontend/apps.py
Normal file
|
@ -0,0 +1,5 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class FrontendConfig(AppConfig):
|
||||
name = 'frontend'
|
59
frontend/consumers.py
Normal file
59
frontend/consumers.py
Normal file
|
@ -0,0 +1,59 @@
|
|||
import json
|
||||
|
||||
from channels.db import database_sync_to_async
|
||||
from channels.generic.websocket import AsyncWebsocketConsumer
|
||||
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
|
||||
class ChatConsumer(AsyncWebsocketConsumer):
|
||||
def authenticate(self):
|
||||
return self.scope["user"]
|
||||
|
||||
async def connect(self):
|
||||
self.room_name = self.scope['url_route']['kwargs']['room_name']
|
||||
self.room_group_name = 'chat_%s' % self.room_name
|
||||
|
||||
# Authenticate user
|
||||
user = await database_sync_to_async(self.authenticate)()
|
||||
|
||||
if isinstance(user, AnonymousUser) or not user:
|
||||
await self.close(code=4003)
|
||||
|
||||
# Join room group
|
||||
await self.channel_layer.group_add(
|
||||
self.room_group_name,
|
||||
self.channel_name
|
||||
)
|
||||
|
||||
await self.accept()
|
||||
|
||||
async def disconnect(self, close_code):
|
||||
# Leave room group
|
||||
await self.channel_layer.group_discard(
|
||||
self.room_group_name,
|
||||
self.channel_name
|
||||
)
|
||||
|
||||
# Receive message from WebSocket
|
||||
async def receive(self, text_data):
|
||||
text_data_json = json.loads(text_data)
|
||||
message = text_data_json['message']
|
||||
|
||||
# Send message to room group
|
||||
await self.channel_layer.group_send(
|
||||
self.room_group_name,
|
||||
{
|
||||
'type': 'chat_message',
|
||||
'message': message
|
||||
}
|
||||
)
|
||||
|
||||
# Receive message from room group
|
||||
async def chat_message(self, event):
|
||||
message = event['message']
|
||||
|
||||
# Send message to WebSocket
|
||||
await self.send(text_data=json.dumps({
|
||||
'message': message
|
||||
}))
|
||||
|
0
frontend/migrations/__init__.py
Normal file
0
frontend/migrations/__init__.py
Normal file
3
frontend/models.py
Normal file
3
frontend/models.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from django.db import models
|
||||
|
||||
# Create your models here.
|
7
frontend/routing.py
Normal file
7
frontend/routing.py
Normal file
|
@ -0,0 +1,7 @@
|
|||
from django.urls import re_path
|
||||
|
||||
from . import consumers
|
||||
|
||||
websocket_urlpatterns = [
|
||||
re_path(r'ws/room/(?P<room_name>\w+)/$', consumers.ChatConsumer.as_asgi()),
|
||||
]
|
3
frontend/tests.py
Normal file
3
frontend/tests.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
9
frontend/urls.py
Normal file
9
frontend/urls.py
Normal file
|
@ -0,0 +1,9 @@
|
|||
from django.urls import path
|
||||
|
||||
from frontend.views import DemoView, RoomView
|
||||
|
||||
urlpatterns = [
|
||||
path('', DemoView.as_view()),
|
||||
path('room/<str:room_name>/', RoomView.as_view(), name='room'),
|
||||
]
|
||||
|
16
frontend/views.py
Normal file
16
frontend/views.py
Normal file
|
@ -0,0 +1,16 @@
|
|||
from django.shortcuts import render
|
||||
|
||||
from django.views.generic import TemplateView
|
||||
|
||||
# Create your views here.
|
||||
|
||||
class DemoView(TemplateView):
|
||||
template_name = "frontend/demo.html"
|
||||
|
||||
class RoomView(TemplateView):
|
||||
template_name = "frontend/room.html"
|
||||
|
||||
def get_context_data(self, *args, **kwargs):
|
||||
context = super().get_context_data(*args, **kwargs)
|
||||
context['room_name'] = kwargs["room_name"]
|
||||
return context
|
22
manage.py
Executable file
22
manage.py
Executable file
|
@ -0,0 +1,22 @@
|
|||
#!/usr/bin/env python
|
||||
"""Django's command-line utility for administrative tasks."""
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
"""Run administrative tasks."""
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'pmessage.settings')
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
"Couldn't import Django. Are you sure it's installed and "
|
||||
"available on your PYTHONPATH environment variable? Did you "
|
||||
"forget to activate a virtual environment?"
|
||||
) from exc
|
||||
execute_from_command_line(sys.argv)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
0
pmessage/__init__.py
Normal file
0
pmessage/__init__.py
Normal file
30
pmessage/asgi.py
Normal file
30
pmessage/asgi.py
Normal file
|
@ -0,0 +1,30 @@
|
|||
"""
|
||||
ASGI config for pmessage project.
|
||||
|
||||
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/3.1/howto/deployment/asgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from channels.auth import AuthMiddlewareStack
|
||||
from channels.routing import ProtocolTypeRouter, URLRouter
|
||||
from channels.security.websocket import AllowedHostsOriginValidator
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
import frontend.routing
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'pmessage.settings')
|
||||
|
||||
application = ProtocolTypeRouter({
|
||||
"http": get_asgi_application(),
|
||||
"websocket": AllowedHostsOriginValidator(
|
||||
AuthMiddlewareStack(
|
||||
URLRouter(
|
||||
frontend.routing.websocket_urlpatterns
|
||||
)
|
||||
)
|
||||
),
|
||||
})
|
137
pmessage/settings.py
Normal file
137
pmessage/settings.py
Normal file
|
@ -0,0 +1,137 @@
|
|||
"""
|
||||
Django settings for pmessage project.
|
||||
|
||||
Generated by 'django-admin startproject' using Django 3.1.3.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/3.1/topics/settings/
|
||||
|
||||
For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/3.1/ref/settings/
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = 'e4rgo)yp&*g)#hm@4g6n30&vyqkm8(#-(y&3*mw$%*ju-w7w&#'
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
|
||||
ALLOWED_HOSTS = []
|
||||
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'channels',
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'frontend',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
]
|
||||
|
||||
ROOT_URLCONF = 'pmessage.urls'
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [BASE_DIR / "templates"],
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
'django.template.context_processors.debug',
|
||||
'django.template.context_processors.request',
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = 'pmessage.wsgi.application'
|
||||
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/3.1/ref/settings/#databases
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': BASE_DIR / 'db.sqlite3',
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/3.1/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = 'en-us'
|
||||
|
||||
TIME_ZONE = 'UTC'
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
USE_L10N = True
|
||||
|
||||
USE_TZ = True
|
||||
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/3.1/howto/static-files/
|
||||
|
||||
STATIC_URL = '/static/'
|
||||
|
||||
STATICFILES_DIRS = [
|
||||
BASE_DIR / "static",
|
||||
]
|
||||
|
||||
ASGI_APPLICATION = 'pmessage.asgi.application'
|
||||
|
||||
CHANNEL_LAYERS = {
|
||||
'default': {
|
||||
'BACKEND': 'channels_redis.core.RedisChannelLayer',
|
||||
'CONFIG': {
|
||||
"hosts": [('127.0.0.1', 6379)],
|
||||
},
|
||||
},
|
||||
}
|
22
pmessage/urls.py
Normal file
22
pmessage/urls.py
Normal file
|
@ -0,0 +1,22 @@
|
|||
"""pmessage URL Configuration
|
||||
|
||||
The `urlpatterns` list routes URLs to views. For more information please see:
|
||||
https://docs.djangoproject.com/en/3.1/topics/http/urls/
|
||||
Examples:
|
||||
Function views
|
||||
1. Add an import: from my_app import views
|
||||
2. Add a URL to urlpatterns: path('', views.home, name='home')
|
||||
Class-based views
|
||||
1. Add an import: from other_app.views import Home
|
||||
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
|
||||
Including another URLconf
|
||||
1. Import the include() function: from django.urls import include, path
|
||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||
"""
|
||||
from django.contrib import admin
|
||||
from django.urls import path, include
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
path('', include("frontend.urls")),
|
||||
]
|
16
pmessage/wsgi.py
Normal file
16
pmessage/wsgi.py
Normal file
|
@ -0,0 +1,16 @@
|
|||
"""
|
||||
WSGI config for pmessage project.
|
||||
|
||||
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/3.1/howto/deployment/wsgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'pmessage.settings')
|
||||
|
||||
application = get_wsgi_application()
|
3
requirements.txt
Normal file
3
requirements.txt
Normal file
|
@ -0,0 +1,3 @@
|
|||
django
|
||||
channels
|
||||
channels_redis
|
73
static/frontend/demo/demo.js
Normal file
73
static/frontend/demo/demo.js
Normal file
|
@ -0,0 +1,73 @@
|
|||
(async () => {
|
||||
|
||||
// Load Worker
|
||||
|
||||
openpgp.initWorker({ path: '/static/frontend/openpgp/openpgp.worker.min.js' });
|
||||
|
||||
// Define key details
|
||||
|
||||
const participant1_name = "Test";
|
||||
const participant1_id = "test@rooms.kumi.test";
|
||||
const participant1_passphrase = "super long and hard to guess secret";
|
||||
|
||||
const participant2_name = "More test";
|
||||
const participant2_id = "test2@rooms.kumi.test";
|
||||
const participant2_passphrase = "even longer and harder to guess secret";
|
||||
|
||||
const curve = "ed25519";
|
||||
|
||||
var participant1_options = {
|
||||
userIds: [{ name: participant1_name, email: participant1_id }],
|
||||
curve: curve,
|
||||
passphrase: participant1_passphrase
|
||||
};
|
||||
|
||||
var participant2_options = {
|
||||
userIds: [{ name: participant2_name, email: participant2_id }],
|
||||
curve: curve,
|
||||
passphrase: participant2_passphrase
|
||||
};
|
||||
|
||||
// Generate key
|
||||
|
||||
const { privateKeyArmored: participant1_privkey, publicKeyArmored: participant1_pubkey } = await openpgp.generateKey(participant1_options);
|
||||
|
||||
const { privateKeyArmored: participant2_privkey, publicKeyArmored: participant2_pubkey } = await openpgp.generateKey(participant2_options);
|
||||
|
||||
// Read and decrypt keys
|
||||
|
||||
const { keys: [participant1_readkey] } = await openpgp.key.readArmored(participant1_privkey);
|
||||
await participant1_readkey.decrypt(participant1_passphrase);
|
||||
|
||||
const { keys: [participant2_readkey] } = await openpgp.key.readArmored(participant2_privkey);
|
||||
await participant2_readkey.decrypt(participant2_passphrase);
|
||||
|
||||
// Get public keys
|
||||
|
||||
const participant1_readpubkey = (await openpgp.key.readArmored(participant1_pubkey)).keys[0];
|
||||
const participant2_readpubkey = (await openpgp.key.readArmored(participant2_pubkey)).keys[0];
|
||||
|
||||
// Encrypt message
|
||||
|
||||
const text = "This is encrypted text sent from participant 1 to participant 2.";
|
||||
|
||||
const { data: encrypted } = await openpgp.encrypt({
|
||||
message: openpgp.message.fromText(text),
|
||||
publicKeys: [participant2_readpubkey],
|
||||
privateKeys: [participant1_readkey]
|
||||
});
|
||||
|
||||
console.log(encrypted);
|
||||
|
||||
// Decrypt message
|
||||
|
||||
const { data: decrypted, signatures: signatures } = await openpgp.decrypt({
|
||||
message: await openpgp.message.readArmored(encrypted),
|
||||
publicKeys: [participant1_readpubkey],
|
||||
privateKeys: [participant2_readkey]
|
||||
});
|
||||
|
||||
console.log(decrypted);
|
||||
console.log(await signatures[0].valid);
|
||||
|
||||
})();
|
2
static/frontend/openpgp/openpgp.min.js
vendored
Normal file
2
static/frontend/openpgp/openpgp.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
2
static/frontend/openpgp/openpgp.worker.min.js
vendored
Normal file
2
static/frontend/openpgp/openpgp.worker.min.js
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
/*! OpenPGP.js v4.10.8 - 2020-08-28 - this is LGPL licensed code, see LICENSE/our website https://openpgpjs.org/ for more information. */
|
||||
!function(){return function e(n,r,t){function o(i,s){if(!r[i]){if(!n[i]){var c="function"==typeof require&&require;if(!s&&c)return c(i,!0);if(a)return a(i,!0);var f=new Error("Cannot find module '"+i+"'");throw f.code="MODULE_NOT_FOUND",f}var u=r[i]={exports:{}};n[i][0].call(u.exports,function(e){return o(n[i][1][e]||e)},u,u.exports,e,n,r,t)}return r[i].exports}for(var a="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}}()({1:[function(e,n,r){(function(e){importScripts("openpgp.min.js");var n=e.openpgp,r=[],t=6e4;n.crypto.random.randomBuffer.init(t,function(){return r.length||self.postMessage({event:"request-seed",amount:t}),new Promise(function(e){r.push(e)})}),self.onmessage=function(e){var t,s=e.data||{};switch(s.event){case"configure":t=s.config,Object.keys(t).forEach(function(e){n.config[e]=t[e]});break;case"seed-random":!function(e){e instanceof Uint8Array||(e=new Uint8Array(e));n.crypto.random.randomBuffer.set(e)}(s.buf);var c=r;r=[];for(var f=0;f<c.length;f++)c[f]();break;default:!function(e,r,t){if("clear-key-cache"===r)return Array.from(o.values()).forEach(e=>{e.isPrivate()&&e.clearPrivateParams()}),o.clear(),void i({id:e,event:"method-return"});if("function"!=typeof n[r])return void i({id:e,event:"method-return",err:"Unknown Worker Event"});n.util.restoreStreams(t),(t=n.packet.clone.parseClonedPackets(t,r)).publicKeys&&(t.publicKeys=t.publicKeys.map(a));t.privateKeys&&(t.privateKeys=t.privateKeys.map(a));n[r](t).then(function(r){i({id:e,event:"method-return",data:n.packet.clone.clonePackets(r)})}).catch(function(r){n.util.print_debug_error(r),i({id:e,event:"method-return",err:r.message,stack:r.stack})})}(s.id,s.event,s.options||{})}};const o=new Map;function a(e){const n=e.armor();return o.has(n)?o.get(n):(o.set(n,e),e)}function i(e){self.postMessage(e,n.util.getTransferables(e.data,n.config.zero_copy))}postMessage({event:"loaded"})}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{}]},{},[1]);
|
8
templates/frontend/demo.html
Normal file
8
templates/frontend/demo.html
Normal file
|
@ -0,0 +1,8 @@
|
|||
<html>
|
||||
<head>
|
||||
<script src="/static/frontend/openpgp/openpgp.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<script src="/static/frontend/demo/demo.js"></script>
|
||||
</body>
|
||||
</html>
|
49
templates/frontend/room.html
Normal file
49
templates/frontend/room.html
Normal file
|
@ -0,0 +1,49 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<title>Chat Room</title>
|
||||
</head>
|
||||
<body>
|
||||
<textarea id="chat-log" cols="100" rows="20"></textarea><br>
|
||||
<input id="chat-message-input" type="text" size="100"><br>
|
||||
<input id="chat-message-submit" type="button" value="Send">
|
||||
{{ room_name|json_script:"room-name" }}
|
||||
<script>
|
||||
const roomName = JSON.parse(document.getElementById('room-name').textContent);
|
||||
|
||||
const chatSocket = new WebSocket(
|
||||
'ws://'
|
||||
+ window.location.host
|
||||
+ '/ws/room/'
|
||||
+ roomName
|
||||
+ '/'
|
||||
);
|
||||
|
||||
chatSocket.onmessage = function(e) {
|
||||
const data = JSON.parse(e.data);
|
||||
document.querySelector('#chat-log').value += (data.message + '\n');
|
||||
};
|
||||
|
||||
chatSocket.onclose = function(e) {
|
||||
console.error('Chat socket closed unexpectedly');
|
||||
};
|
||||
|
||||
document.querySelector('#chat-message-input').focus();
|
||||
document.querySelector('#chat-message-input').onkeyup = function(e) {
|
||||
if (e.keyCode === 13) { // enter, return
|
||||
document.querySelector('#chat-message-submit').click();
|
||||
}
|
||||
};
|
||||
|
||||
document.querySelector('#chat-message-submit').onclick = function(e) {
|
||||
const messageInputDom = document.querySelector('#chat-message-input');
|
||||
const message = messageInputDom.value;
|
||||
chatSocket.send(JSON.stringify({
|
||||
'message': message
|
||||
}));
|
||||
messageInputDom.value = '';
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
Loading…
Reference in a new issue