From 3adecc579fa493c382e581e44d788f307f74fe58 Mon Sep 17 00:00:00 2001 From: etrian-dev Date: Thu, 5 May 2022 12:46:15 +0200 Subject: [PATCH] Improved user search + logging + some error handling --- TODO.md | 5 +- chatroom/Admin.py | 4 +- chatroom/Chat.py | 70 +++++++++++++++++---------- chatroom/Msg.py | 57 ++++++++++++++++------ chatroom/Search.py | 21 ++++++-- chatroom/__init__.py | 23 +++++++-- chatroom/db.py | 2 + chatroom/db_schema.sql | 4 +- chatroom/templates/404_not_found.html | 11 +++++ chatroom/templates/admin_console.html | 11 ++++- chatroom/templates/chats.html | 2 +- 11 files changed, 154 insertions(+), 56 deletions(-) create mode 100644 chatroom/templates/404_not_found.html diff --git a/TODO.md b/TODO.md index cc52171..38d0d2e 100644 --- a/TODO.md +++ b/TODO.md @@ -5,10 +5,11 @@ - User detail page - Encryption/decryption of message data with RSA - logout function and proper authentication (maybe sessions) -- Fix msgstore to write into ./instance -- Improve searching (onchange, but clearing the previous matches from the select) - Fix timestamp for message viewing order in messages.html + Needs revision on Chat.py:display_chat +- In msg.py all function that modify a chat (adding a message, deleting one) should update the +last_mod_tm field of the Chat object in the db +- Edit db schema to have a timestamp for the last two attributes - Develop & testing of query maker (in db.py) # Container - Add volume to make source changes shared between host and container diff --git a/chatroom/Admin.py b/chatroom/Admin.py index e27538d..5ab890f 100644 --- a/chatroom/Admin.py +++ b/chatroom/Admin.py @@ -19,7 +19,9 @@ def show_users(): for user in cursor.fetchall(): all_users[user['user_id']] = { "username": user['username'], - "password": user['password']} + "password": user['password'], + "pub_key": [user['pk_e'].hex()[:20] + "...", user['pk_n'].hex()[:20] + "..."], + "priv_key": user['pk_d'].hex()[:20] + "..."} return render_template('admin_console.html', all_users=all_users) except DatabaseError: return "DB error" \ No newline at end of file diff --git a/chatroom/Chat.py b/chatroom/Chat.py index ff8c308..e0ca69e 100644 --- a/chatroom/Chat.py +++ b/chatroom/Chat.py @@ -3,6 +3,8 @@ from datetime import datetime from sqlite3 import Connection, DatabaseError from json import load +import logging + from flask import (Blueprint, request, render_template) from . import db from . import Msg @@ -83,20 +85,21 @@ def create_chat(creator): # error = None matching_users = [] - if request.method == 'POST': + if request.method == 'POST' and 'matching-users' in request.form: # get the username of the user the creator wants to chat with - username = request.form['matching-users'] - # get matching users + # this value exists in the database because checks are made by the search service + # and it's unique because it's returned trough a form select + user_id = request.form['matching-users'] db_conn = db.get_db() try: cursor = db_conn.execute(''' SELECT user_id, username, password FROM Users - WHERE user_id=?;''', [username]) - - # TODO: handle multiple users with the same username + WHERE user_id=?;''', [user_id]) + # user_id is primary key, so at most one tuple is fetched from the db match = cursor.fetchone() if match is not None: + # FIXME: probably should modify the db schema for the last two attributes -> timestamp cursor.execute(''' INSERT INTO Chats(participant1, participant2, creation_tm, last_mod_tm) VALUES (?,?,datetime(),datetime()); @@ -104,6 +107,7 @@ def create_chat(creator): db_conn.commit() cursor.close() + logging.info(f"Chat between {creator} and {match['user_id']} created at {datetime.now()}") return redirect(url_for('chat.display_chat', user=creator, other=match['user_id'])) except DatabaseError: @@ -117,33 +121,46 @@ def display_chat(user, other): chat_info['this_user_id'] = user chat_info['other_user_id'] = other db_conn = db.get_db() + # fetch user IDs and usernames # of the current user and the other participant in the chat - cur = db_conn.execute(''' - SELECT username FROM Users WHERE user_id = ?; - ''', [user]) - chat_info['this_user'] = cur.fetchone()['username'] - cur.execute(''' - SELECT user_id,username FROM Users WHERE user_id = ?; - ''', [other]) - chat_info['other_user'] = cur.fetchone()['username'] - # fetch the chat + try: + cur = db_conn.execute(''' + SELECT username FROM Users WHERE user_id = ?; + ''', [user]) + chat_info['this_user'] = cur.fetchone()['username'] + cur.execute(''' + SELECT user_id,username FROM Users WHERE user_id = ?; + ''', [other]) + chat_info['other_user'] = cur.fetchone()['username'] + except: + logging.error(f"Either user {user} or {other} not found in the database") + return render_template('404_not_found.html') + + # build breadcrumb + breadcrumb = {} + breadcrumb['home'] = url_for('chat.home_user', user_id=user) + breadcrumb[chat_info['other_user']] = url_for( + 'chat.display_chat', user=user, other=other) + chat_info['breadcrumb'] = breadcrumb + + # fetch the chat + # (the or is needed because we don't know who created the chat) cur.execute(''' SELECT * FROM Chats WHERE (participant1 = ? AND ? = participant2) OR (participant1 = ? AND participant2 = ?); ''', [user, other, other, user]) chat = cur.fetchone() + # check if the chat exists + if chat is None: + logging.error(f"Chat between {user} and {other} not found") + return render_template('404_not_found.html') # fetch all messages sent by the other user cur.execute(''' SELECT * FROM Messages WHERE chatref = ? AND sender == ? ; ''', [chat['chat_id'], other]) msgs_encoded = cur.fetchall() - # build breadcrumb - breadcrumb = {} - breadcrumb['home'] = url_for('chat.home_user', user_id=user) - breadcrumb[chat_info['other_user']] = url_for( - 'chat.display_chat', user=user, other=other) - chat_info['breadcrumb'] = breadcrumb + # decode messages messages = [] for msg in msgs_encoded: @@ -159,8 +176,9 @@ def display_chat(user, other): {'msg_id': msg['msg_id'], 'sender': sender, 'receiver': receiver, - 'data': Msg.decrypt_message(user, msg['msg_data'])}) - # fetch all messages sent by this user + 'data': Msg.decrypt_message(user, msg['msg_data']), + 'tstamp': msg['tstamp']}) + # fetch all messages sent by this user from the msgstore try: with open(Msg.get_msgstore(user, other), 'r') as msgstore: sent_messages = load(msgstore) @@ -169,12 +187,14 @@ def display_chat(user, other): {'msg_id': msg['msg_id'], 'sender': chat_info['this_user'], 'receiver': chat_info['other_user'], - 'data': msg['data']}) - print(sent_messages) + 'data': msg['data'], + 'tstamp': msg['tstamp']}) except FileNotFoundError: pass # no messages sent by this user yet + messages.sort(key=lambda x: x['tstamp']) chat_info['messages'] = messages + logging.info(f"User {user} requested the chat with {other}: {len(messages)} messages served") return render_template('messages.html', **chat_info) diff --git a/chatroom/Msg.py b/chatroom/Msg.py index bfc5d18..c285651 100644 --- a/chatroom/Msg.py +++ b/chatroom/Msg.py @@ -7,7 +7,9 @@ def __init__(self, data: str, stamp: int): from . import db from sqlite3 import Connection, Cursor, DatabaseError from json import (load, loads, dump, dumps) -from os import fspath, path, SEEK_END +from os import fspath, path, SEEK_END, SEEK_SET +from time import time +import logging from flask import ( Blueprint, flash, g, current_app, redirect, render_template, request, session, url_for, make_response @@ -87,34 +89,57 @@ def send_message(sender, recipient): # encrypt encrypted_msg = encrypt_msg(recipient, msg_data) + + # get the timestamp + tstamp = int(time()) + # insert the message cur.execute(''' - INSERT INTO Messages(chatref,sender,recipient,msg_data) - VALUES (?, ?, ?, ?); - ''', [chat_id, sender, recipient, encrypted_msg.to_bytes(length=encrypted_msg.bit_length() // 8 + 1, byteorder='big')]) + INSERT INTO Messages(chatref,sender,recipient,msg_data,tstamp) + VALUES (?, ?, ?, ?, ?); + ''', + [chat_id, + sender, + recipient, + encrypted_msg.to_bytes(length=encrypted_msg.bit_length() // 8 + 1, byteorder='big'), + tstamp]) # store the last row id == msg_id in this case msg_id = cur.lastrowid db_conn.commit() - # fetch the message id + logging.info(f"{sender} sent message {msg_id} to {recipient} at {tstamp}") + # Save the message to the msgstore - msg_obj = {"msg_id": msg_id, "sender": sender, "recipient": recipient, "data": msg_data} + msg_obj = { + "msg_id": msg_id, + "sender": sender, + "recipient": recipient, + "data": msg_data, + "tstamp": tstamp} file_exists = path.exists(get_msgstore(sender,recipient)) if file_exists: with open(get_msgstore(sender, recipient), 'r+b') as msgstore: - # cancel array end character - b = msgstore.seek(-1, SEEK_END) - print('seek = ', b) - msgstore.write((',\n' + dumps(msg_obj) + ' ]').encode(encoding='utf-8')) + # if there are no sent messages from sender to recipient, write a new array + # with one element + r = msgstore.read(2).decode(encoding='utf-8') + if r == '[]': + msgstore.seek(0, SEEK_SET) + msgstore.write(('[ ' + dumps(msg_obj) + ' ]').encode(encoding='utf-8')) + else: + # otherwise a line is added, by extending the array with a comma and a newline + # and then rewriting a ']' at the end + b = msgstore.seek(-1, SEEK_END) + msgstore.write((',\n' + dumps(msg_obj) + ' ]').encode(encoding='utf-8')) else: + # creates and writes the array containing this message with open(get_msgstore(sender, recipient), 'a') as msgstore: msgstore.write('[ ' + dumps(msg_obj) + ' ]') - print('last pos = ', msgstore.tell()) - - print(f"{sender} said to {recipient}: {msg_data}") - # return an empty response, with a 201 Created code - return redirect(url_for('chat.display_chat', user=sender, other=recipient)) except DatabaseError: - return f"Error" + flash(f"Failed to send the message") + except: + flash(f"Some error occurred") + # Redirects the user to the chat page after creating the resource + return redirect(url_for('chat.display_chat', user=sender, other=recipient)) + @blueprint.route('/', methods=['GET', 'PUT']) diff --git a/chatroom/Search.py b/chatroom/Search.py index e5ef604..c10297a 100644 --- a/chatroom/Search.py +++ b/chatroom/Search.py @@ -5,6 +5,7 @@ from datetime import datetime from sqlite3 import Connection, Cursor, DatabaseError from json import dumps +import logging from flask import ( Blueprint, flash, g, redirect, render_template, request, session, url_for @@ -16,11 +17,23 @@ @blueprint.route('/', methods=['GET']) def search_user(): + '''This method allows searching for users in the database. + + It receives a GET request with an URL like + /search/?user=, where the string may be empty (no matches in that case). + The function returns a json array of objects + { "user_id": , "username": } whose username has a prefix + equal to the argument of the query. If the match is in the middle of the string + that user is not added to the array. + ''' + logging.debug(f"search query: {request.args}") matching_users = [] - if request.method == 'GET': + # Does not match any user if the search query is empty + if request.method == 'GET' and 'user' in request.args and len(request.args['user']) > 0 : query = request.args['user'] - print(query) + db_conn = db.get_db() + try: cursor = db_conn.execute(''' SELECT user_id, username @@ -34,8 +47,8 @@ def search_user(): "user_id": row['user_id'], "username": row['username'] }) - print(len(matching_users)) except DatabaseError as err: print('SQLite error: %s' % (' '.join(err.args))) print("Exception class is: ", err.__class__) - return jsonify(matching_users) + logging.debug(f"{len(matching_users)} mathches found: {matching_users}") + return jsonify(matching_users) diff --git a/chatroom/__init__.py b/chatroom/__init__.py index ca0a35e..5aa8ee6 100644 --- a/chatroom/__init__.py +++ b/chatroom/__init__.py @@ -1,4 +1,6 @@ import os +import logging +from datetime import datetime from flask import Flask, render_template @@ -7,21 +9,34 @@ def create_app(testing=None): + app = Flask(__name__, instance_relative_config=True) app.config.from_mapping( SECRET_KEY='dev', DATABASE=os.path.join(app.instance_path, 'chatroom.sqlite'), #EXPLAIN_TEMPLATE_LOADING=True, ) + # ensure the instance folder exists - try: - os.makedirs(app.instance_path) - except OSError: - pass + if not os.path.exists(app.instance_path): + try: + os.makedirs(app.instance_path) + except OSError: + print(f"Failed creating instance directory at {app.instance_path}") + # if testing is None: # app.config.from_pyfile('config.py', silent=True) # else: # app.config.from_mapping(testing) + + # setup logging + logging.basicConfig( + filename=os.path.join(app.instance_path, 'chatroom.log'), + filemode='w', + level=logging.DEBUG, + encoding='utf-8') + logging.info(f"Application launched at {datetime.now()}") + app.register_blueprint(Auth.blueprint) app.register_blueprint(Chat.blueprint) app.register_blueprint(Msg.blueprint) diff --git a/chatroom/db.py b/chatroom/db.py index d81c88e..4ace77f 100644 --- a/chatroom/db.py +++ b/chatroom/db.py @@ -2,6 +2,7 @@ # https://flask.palletsprojects.com/en/2.0.x/tutorial/database/ import sqlite3 +import logging import click from flask import current_app, g @@ -28,6 +29,7 @@ def init_db(): with current_app.open_resource('db_schema.sql', 'r') as f: contents = f.read() gdb.executescript(contents) + logging.info(f"Initialized database at {current_app.config['DATABASE']} from script {f.name}") # TODO: untested def querydb(query: str, args: list): '''Generator that queries the database and returns the resulting rows. diff --git a/chatroom/db_schema.sql b/chatroom/db_schema.sql index 7b59528..53f371d 100644 --- a/chatroom/db_schema.sql +++ b/chatroom/db_schema.sql @@ -33,5 +33,7 @@ CREATE TABLE Messages ( sender INTEGER REFERENCES Users(user_id), recipient INTEGER REFERENCES Users(user_id), msg_data BLOB, - CHECK (sender != recipient) + tstamp INTEGER NOT NULL, + CHECK (sender != recipient), + CHECK (tstamp > 0) ); diff --git a/chatroom/templates/404_not_found.html b/chatroom/templates/404_not_found.html new file mode 100644 index 0000000..1d1faf7 --- /dev/null +++ b/chatroom/templates/404_not_found.html @@ -0,0 +1,11 @@ + + + + + + Login + + + 404 + + \ No newline at end of file diff --git a/chatroom/templates/admin_console.html b/chatroom/templates/admin_console.html index 9dd30cb..2d0f429 100644 --- a/chatroom/templates/admin_console.html +++ b/chatroom/templates/admin_console.html @@ -8,8 +8,15 @@ {% for usr in all_users.keys() %} -
- {{ all_users[usr]['username'] }} : {{ all_users[usr]['password'] }} +
+
{{ all_users[usr]['username'] }}
+
+
    +
  • password: {{ all_users[usr]['password']}}
  • +
  • public key: {{ all_users[usr]['pub_key']}}
  • +
  • private key: {{ all_users[usr]['priv_key']}}
  • +
+
{% endfor %} diff --git a/chatroom/templates/chats.html b/chatroom/templates/chats.html index de5d62f..4040bb1 100644 --- a/chatroom/templates/chats.html +++ b/chatroom/templates/chats.html @@ -34,10 +34,10 @@