From 9d0dd54743c702a49ff5391197725305744a19b1 Mon Sep 17 00:00:00 2001 From: Kamil Marczak Date: Sun, 21 Mar 2021 05:53:16 +0100 Subject: [PATCH] readme --- README.md | 131 ++++++++++++------ cert.sh | 72 ---------- data/nginx/conf_ssl.d/nginx.conf | 6 +- docker-compose.dev.yml | 6 - docker-compose.yml | 27 ---- .../message_storage/Natalia-AND-root.txt | 2 +- messenger/models.py | 92 +----------- readme-static/sit-logo.png | Bin 0 -> 13116 bytes sit-logo.png | Bin 0 -> 13116 bytes 9 files changed, 92 insertions(+), 244 deletions(-) delete mode 100644 cert.sh create mode 100644 readme-static/sit-logo.png create mode 100644 sit-logo.png diff --git a/README.md b/README.md index de70310..c809cd6 100644 --- a/README.md +++ b/README.md @@ -1,48 +1,87 @@ -sudo docker-compose exec web python manage.py initial_deployment - -sudo docker-compose exec web python manage.py shell_plus --ipython -sudo docker-compose exec web python manage.py createsuperuser -sudo docker-compose exec web python manage.py generateschema --file openapi-schema.yml -sudo docker-compose exec web python manage.py export route_registered_with_the_router - -**ipython shell**
-sudo docker-compose exec web python manage.py shell - - -sudo docker-compose exec web sh - -**clear data**
-sudo docker system prune -a -sudo docker volume prune -sudo docker-compose exec web python manage.py flush - -https://linuxize.com/post/how-to-remove-docker-images-containers-volumes-and-networks/ -For docker-compose the correct way to remove volumes would be docker-compose down --volumes or docker-compose down --rmi all --volumes - -sudo docker-compose ps -sudo docker container ls -a -sudo docker container stop 2fe70c7c1839 -sudo docker container rm 2fe70c7c1839 -sudo docker system prune -a - -sudo docker volume ls - -**pg admin**
-https://towardsdatascience.com/how-to-run-postgresql-and-pgadmin-using-docker-3a6a8ae918b5 - - -deployment on digital ocean: - -(local) git archive --format tar --output ./project.tar main -(local) rsync ./project.tar root@$DIGITAL_OCEAN_IP_ADDRESS:/tmp/project.tar -(ssh) rm -rf /app/* && tar -xf /tmp/project.tar -C /app -(ssh) cd /app/ -(ssh) docker-compose -f /app/docker-compose.prod.yml build -(ssh) docker-compose -f /app/docker-compose.prod.yml up - - -sudo kill $(sudo lsof -t -i:80) - -ssh user@server -o ServerAliveInterval=15 +![alt text](sit-logo.png "Title" ) + +# SIT messenger - backend + +> SIT Messenger is a fully encrypted chat application. SIT Messenger Frontend is Nuxt.js web client for backend Django Rest API service. + +### 🏠 [Homepage](https://sit-messenger.com/) + +### ✨ [Demo](https://sit-messenger.com/) + +## Installation and usage in production +1. Clone repo +2. Install docker-compose +3. Build images + ``` + docker-compose build + ``` +4. Run containers + ``` + docker-compose up + ``` +5. application will be available on ports 80 (http) adn 443 (htpps) + +## Installation and usage in production +1-2 ame a in production +3. Build images + ``` + docker-compose -f docker-compose.dev.yml build + ``` +4. Run containers + ``` + docker-compose -f docker-compose.dev.yml up + ``` +5. application will be available http://localhost:8000/ + + +## Application structure + +Application component are packed inside docker-compose. Inside docker there are several linked services as separate +containers: + +- #### nginx + A web server, used for the purpose of reverse proxy, load balancer (jointly with Gunicorn in "web" container, later in + the text), and parse HTTPS protocol (jointly with "certbot" container ) +- #### certbot + A software tool for automatically using Let’s Encrypt certificates on manually-administrated websites to enable HTTPS +- #### web + The Main container with Django Rest Framework application. This container is collection of application organised to + manage encrypted data hosted in storage, data from a database, integrate applications n from other contenders and + finally parse Rest API as entry point +- #### db + A Postgres relational database. +- #### redis + An in-memory data structure store used to store cache +- #### rabbitmq + A message-broker used by celery_worker container. +- #### celery_worker + An asynchronous task and job queue. Celery worker mange caches by distributed message passing. Celery worker use " + redis" container ad storage and "rabbitmq" container as message-broker +- #### celery_beat + A specific usage of celery to manage recurring job queue in given time intervals. +- flower. A web based tool for monitoring and administrating Celery clusters. + +## Data encryption: + +- chats (which contain messages) are store as models.FileField where chats are stored as encrypted binary files and + database store location to file +- everytime messages are saved to/read from storage, data is encrypted / decrypted by individual (for each chat). + Cryptography is held under Fernet protocol provided by Cryptography + framework [Cryptography Framework](https://cryptography.io/en/latest/fernet.html). +- It is not completed in this version but there are complited snipets inside aplication to provide encryption by tokens + that base on, individual uudi created and stored on client side and salt created and stored on Django application. + Lack of implementation is caused by the limitation of "SIT Messenger Frontend" the JavaScript client of Rest + API [SIT Messenger Frontend](https://github.com/kamil1marczak/SIT-messenger-frontend) + +## Author + +👤 **Kamil Marczak** + +* Github: [@kamil1marczak](https://github.com/kamil1marczak) +* LinkedIn: [@kamil-marczak-71765b48](https://linkedin.com/in/kamil-marczak-71765b48) + +## Show your support + +Give a ⭐️ if this project helped you! \ No newline at end of file diff --git a/cert.sh b/cert.sh deleted file mode 100644 index 8cfa3a4..0000000 --- a/cert.sh +++ /dev/null @@ -1,72 +0,0 @@ -#!/bin/bash - -domain=api.sit-messenger.com -rsa_key_size=4096 -data_path="./data/certbot" -email="kamil1marczak@gmail.com" # Adding a valid address is strongly recommended -staging=1 - -#docker-compose build cert - -if [ -d "$data_path" ]; then - read -p "Existing data found for $domain. Continue and replace existing certificate? (y/N) " decision - if [ "$decision" != "Y" ] && [ "$decision" != "y" ]; then - exit - fi -fi - -if [ ! -e "$data_path/conf/options-ssl-nginx.conf" ] || [ ! -e "$data_path/conf/ssl-dhparams.pem" ]; then - echo "### Downloading recommended TLS parameters ..." - mkdir -p "$data_path/conf" - curl -s https://raw.githubusercontent.com/certbot/certbot/master/certbot-nginx/certbot_nginx/_internal/tls_configs/options-ssl-nginx.conf >"$data_path/conf/options-ssl-nginx.conf" - curl -s https://raw.githubusercontent.com/certbot/certbot/master/certbot/certbot/ssl-dhparams.pem >"$data_path/conf/ssl-dhparams.pem" - echo -fi - - -echo "### Removing old certificate for $domain ..." -docker-compose run --rm --entrypoint "\ -rm -Rf /etc/letsencrypt/live/$domain && \ -rm -Rf /etc/letsencrypt/archive/$domain && \ -rm -Rf /etc/letsencrypt/renewal/$domain.conf" cert -echo - - -if [ $staging != "0" ]; then staging_arg="--staging"; fi - -#docker-compose run --rm --entrypoint " -# echo 'dns_digitalocean_token = 191939a013dfd3a435fa8de7965ae49c5134426bfe7aae6694ed700631341809' > certbot-creds.ini \ -# chmod go-rwx certbot-creds.ini -#" - -docker-compose run --rm --entrypoint "\ - certbot certonly\ - -m kamil1marczak@gmail.com \ - --staging \ - --force-interactive \ - --expand \ - --no-eff-email \ - --agree-tos \ - --dns-digitalocean \ - --dns-digitalocean-credentials certbot-creds.ini \ - --dns-digitalocean-propagation-seconds 20 \ - -d api.sit-messenger.com" cert -echo - -docker-compose run --rm --entrypoint "\ - certbot certonly\ - -m kamil1marczak@gmail.com \ - --force-interactive \ - --no-eff-email \ - --agree-tos \ - --dns-digitalocean \ - --dns-digitalocean-credentials certbot-creds.ini \ - -d \*.sit-messenger.com" cert -echo - - -echo "### Starting nginx ..." -docker-compose up --force-recreate -d -echo - - diff --git a/data/nginx/conf_ssl.d/nginx.conf b/data/nginx/conf_ssl.d/nginx.conf index 8350e17..3212686 100644 --- a/data/nginx/conf_ssl.d/nginx.conf +++ b/data/nginx/conf_ssl.d/nginx.conf @@ -57,9 +57,11 @@ server { proxy_set_header X-Forwarded-Proto $scheme; } - location /flower/ { + + location /flower { rewrite ^/flower/(.*)$ /$1 break; - proxy_pass http://example.com:5555; + proxy_pass http://flower:5555; + proxy_set_header X-Real-IP $remote_addr; proxy_set_header Host $host; } diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index f0e2efb..9da3c61 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -34,17 +34,11 @@ services: context: . dockerfile: ./compose/local/django/Dockerfile image: sit_core_web - # this is shell script run the service command: /start - # so if we change code on host, code in docker container would also be changed volumes: - .:/app -# - staticfiles:/app/staticfiles -# - mediafiles:/app/mediafiles - # this is the Django dev server default number ports: - 8000:8000 - # we use this file to manage env variables of our project env_file: - .env/dev.env depends_on: diff --git a/docker-compose.yml b/docker-compose.yml index 53efcaa..b4db33b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -20,7 +20,6 @@ services: depends_on: - web - flower -# - frontend certbot: image: certbot/dns-digitalocean @@ -30,32 +29,6 @@ services: - ./data/certbot/www:/var/www/certbot entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 5d & wait $${!}; done;'" -# cert: -# build: -# context: . -# dockerfile: ./compose/production/cert/Dockerfile -# restart: unless-stopped -# volumes: -# - ./data/certbot/conf:/etc/letsencrypt -# - ./data/certbot/www:/var/www/certbot -# entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 5d & wait $${!}; done;'" -# entrypoint: "/bin/sh -c 'certbot certonly -m kamil1marczak@gmail.com --force-interactive --expand --no-eff-email --agree-tos --dns-digitalocean --dns-digitalocean-credentials certbot-creds.ini --dns-digitalocean-propagation-seconds 20 -d api.sit-messenger.com -d www.api.sit-messenger.com;'" - -# frontend: -# image: sit_core_frontend -# build: ./frontend -# # volumes: -# # - ./frontend/:/user/src/app/ -# # expose: -# # - 3000 -# # ports: -# # - 3000:3000 -# depends_on: -# - web -# command: ./start-dev -# env_file: -# - .env/prod.env - web: build: diff --git a/mediafiles/message_storage/Natalia-AND-root.txt b/mediafiles/message_storage/Natalia-AND-root.txt index 97b2900..bbbf5c0 100644 --- a/mediafiles/message_storage/Natalia-AND-root.txt +++ b/mediafiles/message_storage/Natalia-AND-root.txt @@ -1 +1 @@ -gAAAAABgUZ9iXEzb89AYhHgFM89mYcN3tU_Qql__KhUJ6j7OzCwJNC6OQYPKHsOY2n0epP6kM2S7KsjzPngG2w9o1fg49uRM8ZI6t7S2WcQbiNu78EDUTMMrmkaNJgkkQK8lYdcW4IidZYGcHizX5zwb_QzjTKW2O9HUd7dkDvbB4xhWJwxCGhRf9wau2nNMgZ7ppVvx12QyjPSBPUJOBqBhaxNwSO3R7XFyZLcUUV2oTrObPH5a1p4= \ No newline at end of file +gAAAAABgVZjvunnHItYUbbcm6oR8TJixI2IwuEB-0cyg7C0_rTfwIQfPof2hXdhALq-a1wtkNHwIVy4mOcStfmaiAAExiR0tIGRWGBQMfluFjr_51DzrMlKdeEAgSOftNtmlFFTpYk7k6z7lcRhDZGW5x1-KyHOGSH3QEDpkACZxIsPdpMgxCUAJaCs8DBccHKbnM84-imwBItysOHED5ttAMgs8XUP89kxWJGF0Ddg9040HSjA_oIlU7GfqexaV_pkExxmh-7zDgM0mw2FuRpwcUbQW_erlq7UFm0jd8Gp3cuF0FfKnAlT7R2p5cuJv5njDjipBJ9_jKLIWC0NKPtOJ2uuoT1CSGl8eV8FM48TFXxd09u0miiiL6XF6z-r8j6NTgGY8UnJmm9PplhRfBECSnqhwyi7QL5dcPjXB6iMPEh7icrBbY_aobNQh4qtbEz3B6kkvX5NGH81_Mrj_m8Ul4PKNrwLa2E_wyBsig6e4I30djCJXSENAn4OrjT-aWAzzL_b6jzRFWyIFkapc5hWPCu0S55MIjf0BB2HZpGuS1aRMQE9P8x1ZiRUo5_FgP8Xl1qjoWjhfKnoREPE0QwsZBPObV1B2P78ilWJqlJ7ycIgoGfY9oUsZB7QHhygngaFqr52WI9c7I9ZUCEVoVXhnIwcyNSsahTDuHmBkFO9asfky3Qd8f4bi9S9n6hDPxTG1uOrU3PHc-dqwF5DD7C-qNyRu84kLmgmD8yPxQj5bEOdN7DytciDTFIXTzIjPfT6FPCpd-YGxF96S00VhtNDJxBIZkp24hgsyLTZCOhyP36xGSExGqitwU-AZS3VYuqC_Y0by3Omy \ No newline at end of file diff --git a/messenger/models.py b/messenger/models.py index e4940f5..e258d51 100644 --- a/messenger/models.py +++ b/messenger/models.py @@ -1,40 +1,20 @@ -import itertools import os - from django.core.files.base import ContentFile from django.db import models -from django.contrib.auth.models import AbstractUser, User -from django.db.models import Count, Q -from datetime import datetime, timedelta +from django.contrib.auth.models import AbstractUser +from datetime import datetime import pytz import uuid -from django.utils.decorators import method_decorator -from django.utils.functional import cached_property from django.core.files.storage import FileSystemStorage -from django.db.models.signals import post_save -from django.dispatch import receiver from django.conf import settings -from django.utils.translation import gettext as _ -import json from django.core.cache import cache -# Third-party -from django.views.decorators.cache import never_cache - -# from messenger.crypt import Cryptographer -# from messenger.storage import MessagesSystemStorage, FILE_ROOT from messenger.utils import get_bytes, generate_password_key - -import base64 from cryptography.fernet import Fernet -from cryptography.hazmat.primitives import hashes -from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC import logging - logger = logging.getLogger(__name__) - class CustomUser(AbstractUser): pass id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) @@ -47,7 +27,6 @@ def add_friend(self, users_code_string): return self.friends.all() def __str__(self): - # return str(self.id) return str(self.username) @@ -79,11 +58,6 @@ class Crypt(models.Model): @property def chat_password(self): return None - # - # @chat_password.setter - # def user_password(self, password): - # - @property def fernet(self): @@ -104,7 +78,6 @@ def decrypt(self, encrypted_message): decrypted_message = self.fernet.decrypt(b_message) return decrypted_message - def __str__(self): return str(self.id) @@ -113,26 +86,17 @@ class ChatDataManager(models.Manager): def create_chat(self, users_code): password_code = uuid.uuid4() - # users_code = users_code_string.split(', ') - # users = Profile.objects.filter(code__in=users_code) users = CustomUser.objects.filter(id__in=users_code) now = datetime.now(pytz.utc) message = dict(datetime=now.isoformat(), user="", message="Beginning of messages") message_data = dict(messages=[message, ]) crypto_tool = Crypt.objects.create_crypto() - # crypto_tool.save() - - # crypto_tool = Cryptographer(password_code.bytes) message_data_encrypted = crypto_tool.encrypt(str(message_data)) - message_file = ContentFile(message_data_encrypted) message_file.name = '-AND-'.join([str(elem) for elem in users]) + '.txt' - # crypto = Crypt(code=password_code) - # crypto.save() chat = ChatData(message_file=message_file, crypto=crypto_tool) chat.save() chat.owner.set(users) - return chat def get_queryset(self): @@ -141,36 +105,21 @@ def get_queryset(self): class ChatData(OwnedModel): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) - # code = models.UUIDField(default=uuid.uuid4) created = models.DateTimeField(auto_now_add=True) updated = models.DateTimeField(auto_now=True) - # users = models.ManyToManyField(User) - # message_file = models.FileField(upload_to='message_storage') message_file = models.FileField(storage=FileSystemStorage(location=settings.MEDIA_ROOT), upload_to='message_storage') crypto = models.ForeignKey(Crypt, on_delete=models.CASCADE) - - # message_file = models.FileField(storage=MessagesSystemStorage('storage')) - # message_file = models.FileField(upload_to='storage') - objects = ChatDataManager() class Meta: ordering = ['-updated'] - # constraints = [ - # models.UniqueConstraint(fields=['app_uuid', 'version_code'], name='unique appversion') - # ] - - # @property - # def users_name(self): - @property def message_data(self): encrypted_messages = self.message_file.read() self.message_file.seek(0) message_dict = eval(self.crypto.decrypt(encrypted_messages)) - # message_dict = json.loads(self.crypto_tool.decrypt(self.message_file.read())) if self.message_file else dict if message_dict is None: message_dict = dict() message_dict['messages'] = list() @@ -187,13 +136,9 @@ def add_message(self, new_message_str, user=""): now = datetime.now(pytz.utc) message = dict(datetime=now.isoformat(), user=user, message=new_message_str) messages['messages'].append(message) - # messages[str(now)] = new_message_str - # new_messages = messages.update(new_message_str) cache.delete(f'messages.{id}') cache.set(f'messages.{id}', messages, timeout=None) - # logger.error(f'message add : {str(messages)}') - return now @property @@ -212,14 +157,9 @@ def update_file_from_cache(self): messages_db = self.message_data cache_messages = cache.get(f'messages.{id}') - messages_concat = messages_db['messages'] + cache_messages['messages'] message_data = dict(messages=messages_concat) - - # logger.error(f'messages : {str(messages)}') - logger.error(f'cache message: {str(messages_concat)}') - # logger.error(f'id : {str(id)}') messages_encrypted = self.crypto.encrypt(str(message_data)) old_filename = self.message_file.name @@ -233,33 +173,5 @@ def update_file_from_cache(self): self.save(update_fields=['message_file']) cache.delete(f'messages.{id}') - # @cached_property - # def users_name(self): - # users = self.user.select_related('user').all() - # users_string = ', '.join([str(elem.user) for elem in users]) - # - # return users_string - def __str__(self): return str(self.id) - -# @receiver(post_save, sender=ChatData) -# def create_chat_code(sender, instance, created, **kwargs): -# if created: -# Profile.objects.create(user=instance) -# -# -# @receiver(post_save, sender=ChatData) -# def save_chat_code(sender, instance, **kwargs): -# instance.profile.save() - - -# class OwnedModel(models.Model): -# owner = models.ForeignKey(settings.AUTH_USER_MODEL, -# on_delete=models.CASCADE) -# -# class Meta: -# abstract = True -# -# class Belonging(OwnedModel): -# name = models.CharField(max_length=100) diff --git a/readme-static/sit-logo.png b/readme-static/sit-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..8d72e4615a0796a4a4b5c884008c061aa86fd32d GIT binary patch literal 13116 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