Initial Commit
This commit is contained in:
7
Client/ClientTempMail.py
Normal file
7
Client/ClientTempMail.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# This is a class to help the client side with holding the mail structure that mimics the one used by the server
|
||||||
|
class ClientTempMail:
|
||||||
|
def __init__(self):
|
||||||
|
self.from_address = None
|
||||||
|
self.to_address_list = []
|
||||||
|
self.subject = None
|
||||||
|
self.body = None
|
546
Client/ClientThread.py
Normal file
546
Client/ClientThread.py
Normal file
@ -0,0 +1,546 @@
|
|||||||
|
import queue
|
||||||
|
import socket
|
||||||
|
import selectors
|
||||||
|
import time
|
||||||
|
from datetime import datetime
|
||||||
|
from threading import Thread
|
||||||
|
from time import sleep
|
||||||
|
|
||||||
|
import ClientTempMail
|
||||||
|
import Crypto
|
||||||
|
|
||||||
|
|
||||||
|
# Included the unix timestamp function from the DBConn file so we dont have to use the entire class just for this
|
||||||
|
def get_unix_timestamp(seconds_offset=0) -> int:
|
||||||
|
"""
|
||||||
|
Function to get the current unix timestamp to sim the NOW() sql method, seconds offset will pull a
|
||||||
|
past of future timestamp
|
||||||
|
"""
|
||||||
|
return int(time.time()) + seconds_offset
|
||||||
|
|
||||||
|
|
||||||
|
class ClientThread(Thread):
|
||||||
|
"""Class for dealing with the threaded actions of the client app"""
|
||||||
|
def __init__(self, current_socket: socket, address: tuple):
|
||||||
|
Thread.__init__(self)
|
||||||
|
|
||||||
|
self.__address = address
|
||||||
|
self.__socket: socket.socket = current_socket
|
||||||
|
self.__selector = selectors.DefaultSelector()
|
||||||
|
|
||||||
|
self.__buffer_in = queue.Queue()
|
||||||
|
self.__buffer_out = queue.Queue()
|
||||||
|
|
||||||
|
# flag for the current network status, -1 for awaiting request, 0 for awaiting response, 1 for ready
|
||||||
|
self.current_status = -1
|
||||||
|
self.state = "INIT"
|
||||||
|
self.__init_timeout = get_unix_timestamp()
|
||||||
|
self.last_response_content = []
|
||||||
|
|
||||||
|
# Login and User variables
|
||||||
|
self.__session_ref = None
|
||||||
|
self.login_username = None
|
||||||
|
self.__user_full_name = None
|
||||||
|
self.__user_email = None
|
||||||
|
self.__user_role = None
|
||||||
|
self.initialised = False
|
||||||
|
|
||||||
|
# Mail class to hold the mail data structure that mimics the server version
|
||||||
|
self.client_mail_temp: ClientTempMail.ClientTempMail or None = None
|
||||||
|
|
||||||
|
self.__queued_messages = []
|
||||||
|
|
||||||
|
# Handshake and Encryption Variables
|
||||||
|
self.__pending_handshake = False
|
||||||
|
self.__hand_shook = False
|
||||||
|
self.__crypto_manager_receive: Crypto.Receiver or None = None
|
||||||
|
self.__crypto_manager_transmit: Crypto.Transmitter or None = None
|
||||||
|
|
||||||
|
# Active and Heartbeat Vars
|
||||||
|
self.active = True
|
||||||
|
self.__heartbeat_timer = None
|
||||||
|
self.__heartbeat_tick = -120
|
||||||
|
|
||||||
|
self.__selector.register(self.__socket, selectors.EVENT_READ | selectors.EVENT_WRITE, data=None)
|
||||||
|
|
||||||
|
def is_connected(self) -> bool:
|
||||||
|
"""Method for checking if the connection is active, and sending the noop command for the heartbeat"""
|
||||||
|
if not self.__socket:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# If the connection is initialised we can send commands
|
||||||
|
if self.initialised:
|
||||||
|
try:
|
||||||
|
# Send heartbeat NOOP command, this is a background command as to not ruin the flow of the program
|
||||||
|
# since actual SMTP servers work in a specific order
|
||||||
|
self.queue_command("NOOP")
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def get_users_role(self) -> str:
|
||||||
|
"""Method for getting the current users server role (USER or ADMIN)"""
|
||||||
|
return self.__user_role
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
"""Main threads looped run method that listens and writes our network data"""
|
||||||
|
pending_handshake_counter = 0
|
||||||
|
try:
|
||||||
|
while self.active:
|
||||||
|
waiting_commands = self.__selector.select(timeout=1)
|
||||||
|
for key, mask in waiting_commands:
|
||||||
|
try:
|
||||||
|
if mask & selectors.EVENT_READ:
|
||||||
|
self.__read()
|
||||||
|
elif mask & selectors.EVENT_WRITE and not self.__buffer_out.empty():
|
||||||
|
self.__write()
|
||||||
|
# Process any queued messages when an actual message is not being processed
|
||||||
|
elif mask & selectors.EVENT_WRITE and len(self.__queued_messages) > 0:
|
||||||
|
self.__write_queue()
|
||||||
|
except Exception as ex:
|
||||||
|
print(f"Exception hit: {repr(ex)}")
|
||||||
|
self.__close()
|
||||||
|
|
||||||
|
# Init time out makes sure that a "valid" connection it makes will time out if the server
|
||||||
|
# does not respond the way we want it to
|
||||||
|
if self.state == "INIT":
|
||||||
|
if self.__init_timeout < get_unix_timestamp(-40):
|
||||||
|
print("No INIT processed, the connection is most likely at fault.")
|
||||||
|
self.active = False
|
||||||
|
|
||||||
|
# Pending handshake so we can send the AUTH response then set the handshook flag after it has been sent
|
||||||
|
if self.__pending_handshake:
|
||||||
|
if pending_handshake_counter == 1:
|
||||||
|
self.__pending_handshake = False
|
||||||
|
self.__hand_shook = True
|
||||||
|
pending_handshake_counter = 0
|
||||||
|
else:
|
||||||
|
pending_handshake_counter += 1
|
||||||
|
|
||||||
|
# If logged in we can do our Heartbeat NOOP request to faux keep our connection alive
|
||||||
|
if self.__session_ref:
|
||||||
|
if self.__heartbeat_timer < get_unix_timestamp(self.__heartbeat_tick):
|
||||||
|
if not self.is_connected():
|
||||||
|
print("Server did not respond")
|
||||||
|
self.__close()
|
||||||
|
|
||||||
|
if not self.__selector.get_map():
|
||||||
|
break
|
||||||
|
finally:
|
||||||
|
self.__selector.close()
|
||||||
|
|
||||||
|
def __read(self):
|
||||||
|
"""Private method for reading the network data coming into the client"""
|
||||||
|
try:
|
||||||
|
data_in = self.__socket.recv(1024)
|
||||||
|
except BlockingIOError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
if data_in:
|
||||||
|
if self.__hand_shook:
|
||||||
|
# We decode the bytes of the message we receive, this should be a hex string
|
||||||
|
data_in_decoded_string = data_in.decode()
|
||||||
|
# If needed we split the response into chunks so that we can decrypt the multiple blocks that
|
||||||
|
# were sent
|
||||||
|
chunk_size = self.__crypto_manager_receive.get_expected_block_size()
|
||||||
|
data_in_chunks = [self.__crypto_manager_receive.decrypt_string(
|
||||||
|
data_in_decoded_string[i:i + chunk_size]
|
||||||
|
) for i in range(0, len(data_in_decoded_string), chunk_size)]
|
||||||
|
# Join all of the decrypted chunks back together
|
||||||
|
full_data_in = "".join(data_in_chunks)
|
||||||
|
self.__buffer_in.put(full_data_in)
|
||||||
|
else:
|
||||||
|
# Data is treated normally when we have not shaken hands
|
||||||
|
self.__buffer_in.put(data_in.decode())
|
||||||
|
else:
|
||||||
|
raise RuntimeError("Connection Closed")
|
||||||
|
|
||||||
|
self.process_response()
|
||||||
|
|
||||||
|
def __write_queue(self):
|
||||||
|
"""Private method for writing a queue of data with a simulated 'latency' to stop the messages being joined"""
|
||||||
|
# Some SMTP response require the server to send multiple responses per request
|
||||||
|
while len(self.__queued_messages) > 0:
|
||||||
|
# Sleep to simulate some kind of latency to stop the queue to hopefully stop
|
||||||
|
# the response from being added to the previous.
|
||||||
|
sleep(1)
|
||||||
|
try:
|
||||||
|
self.process_silent_command(self.__queued_messages.pop(0))
|
||||||
|
response = self.__buffer_out.get_nowait()
|
||||||
|
except Exception:
|
||||||
|
response = None
|
||||||
|
|
||||||
|
if response:
|
||||||
|
# This would sometimes interrupt the users input because a queued write would happen during it
|
||||||
|
# print(f"Sending Queued response to Client: {self.__address}")
|
||||||
|
try:
|
||||||
|
self.__do_write(response)
|
||||||
|
except BlockingIOError as ex:
|
||||||
|
print(f"IO Error: {repr(ex)}")
|
||||||
|
pass
|
||||||
|
|
||||||
|
def __write(self):
|
||||||
|
"""Private method for doing a single write to the socket"""
|
||||||
|
try:
|
||||||
|
response = self.__buffer_out.get_nowait()
|
||||||
|
except Exception:
|
||||||
|
response = None
|
||||||
|
|
||||||
|
if response:
|
||||||
|
print(f"Sending data: {repr(response)} to {self.__address}")
|
||||||
|
try:
|
||||||
|
self.__do_write(response)
|
||||||
|
except BlockingIOError as ex:
|
||||||
|
print(f"Could not send data: {repr(ex)}")
|
||||||
|
pass
|
||||||
|
|
||||||
|
def __do_write(self, response: bytes) -> None:
|
||||||
|
"""Shared Private method to do the actual write to the socket"""
|
||||||
|
# Check if we have shook hands
|
||||||
|
if self.__hand_shook:
|
||||||
|
# Get the max chunk size for the key we generated in case we need to split the request down
|
||||||
|
chunk_size = self.__crypto_manager_transmit.get_usable_byte_length()
|
||||||
|
response_chunks = [self.__crypto_manager_transmit.encrypt_string(response[i:i + chunk_size])
|
||||||
|
for i in range(0, len(response), chunk_size)]
|
||||||
|
full_response = b"".join(response_chunks)
|
||||||
|
self.__socket.send(full_response)
|
||||||
|
else:
|
||||||
|
self.__socket.send(response)
|
||||||
|
|
||||||
|
# Set the heartbeat timer since we just sent a command and wont need to send a noop again
|
||||||
|
self.__heartbeat_timer = get_unix_timestamp()
|
||||||
|
|
||||||
|
def process_command(self, command: str) -> None:
|
||||||
|
"""Method to Process the command and add it into the buffer, we also set the client to read mode"""
|
||||||
|
self.current_status = 0
|
||||||
|
self.__buffer_out.put(command.encode())
|
||||||
|
|
||||||
|
def process_silent_command(self, command: str) -> None:
|
||||||
|
"""Method to silently add a command to the buffer, this does not cause the client to enter read mode"""
|
||||||
|
self.__buffer_out.put(command.encode())
|
||||||
|
|
||||||
|
def queue_command(self, command: str) -> None:
|
||||||
|
"""Method to add a command to the queued message array for queued writing"""
|
||||||
|
self.__queued_messages.append(command)
|
||||||
|
|
||||||
|
def process_response(self):
|
||||||
|
"""Method to process the response from the server and to set the client to the required state"""
|
||||||
|
current_data = self.__buffer_in.get()
|
||||||
|
|
||||||
|
# Split the response string by the first space, this should give us a response code and response data
|
||||||
|
response_parts = current_data.split(" ", 1)
|
||||||
|
if len(response_parts) >= 2:
|
||||||
|
|
||||||
|
response_code = int(response_parts[0])
|
||||||
|
|
||||||
|
# If the response is not from the heartbeat print it
|
||||||
|
if response_code != 295:
|
||||||
|
print(f"Response: {repr(current_data)}")
|
||||||
|
|
||||||
|
if len(response_parts) > 1:
|
||||||
|
response_data = response_parts[1]
|
||||||
|
else:
|
||||||
|
response_data = None
|
||||||
|
|
||||||
|
# Check the current state and respond accordingly
|
||||||
|
if self.state == "INIT":
|
||||||
|
if response_code == 220:
|
||||||
|
self.state = "HELO"
|
||||||
|
# Initialised, now send the HELO command
|
||||||
|
self.process_command("HELO " + self.__address[0])
|
||||||
|
self.current_status = 0
|
||||||
|
self.initialised = True
|
||||||
|
else:
|
||||||
|
print("No response from the server...")
|
||||||
|
self.__close()
|
||||||
|
|
||||||
|
elif self.state == "HELO":
|
||||||
|
if response_code == 250:
|
||||||
|
# HELO responded, send the AUTH command
|
||||||
|
self.state = "AUTH"
|
||||||
|
self.current_status = 0
|
||||||
|
|
||||||
|
elif self.state == "AUTH":
|
||||||
|
if not self.__hand_shook:
|
||||||
|
if response_code == 570:
|
||||||
|
# set up the RSA encryption variables for encrypting the connection
|
||||||
|
self.__crypto_manager_transmit = Crypto.Transmitter(response_data)
|
||||||
|
self.__crypto_manager_receive = Crypto.Receiver()
|
||||||
|
self.__crypto_manager_receive.generate_keys()
|
||||||
|
# Send our public key back to the server
|
||||||
|
self.process_command("AUTH " + self.__crypto_manager_receive.get_public_key_pair())
|
||||||
|
# Set the client to a pending handshake
|
||||||
|
self.__pending_handshake = True
|
||||||
|
else:
|
||||||
|
if response_code == 250:
|
||||||
|
# Auth key was sent successfully, start the confirmation handshake
|
||||||
|
self.process_command("HSHK " + response_data)
|
||||||
|
self.state = "HSHK"
|
||||||
|
|
||||||
|
elif self.state == "HSHK":
|
||||||
|
if response_code == 250:
|
||||||
|
# The validation handshake was current, start the login state and set the write
|
||||||
|
self.state = "LOGI"
|
||||||
|
self.current_status = -1
|
||||||
|
|
||||||
|
elif self.state == "LOGI":
|
||||||
|
if response_code == 250:
|
||||||
|
# Login was successful, wait for the secondary role response so we can tell
|
||||||
|
# if the user is and ADMIN or USER
|
||||||
|
self.current_status = 0
|
||||||
|
elif response_code == 265:
|
||||||
|
self.__session_ref = response_data
|
||||||
|
self.current_status = 0
|
||||||
|
# Send the VRFY command to the get logged in users name and email address
|
||||||
|
self.state = "VRFY"
|
||||||
|
self.process_command("VRFY " + self.login_username)
|
||||||
|
elif response_code == 421:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
# Bad login, try again
|
||||||
|
self.last_response_content.clear()
|
||||||
|
self.last_response_content.append(response_data)
|
||||||
|
self.current_status = -1
|
||||||
|
|
||||||
|
elif self.state == "VRFY":
|
||||||
|
if response_code == 261:
|
||||||
|
self.__user_role = response_data
|
||||||
|
elif response_code == 250:
|
||||||
|
# Process the email address that gets sent back with the VRFY response
|
||||||
|
if '@' in response_data:
|
||||||
|
if '<' in response_data:
|
||||||
|
start_bracket_index = response_data.find('<')
|
||||||
|
end_bracket_index = response_data.rfind('>')
|
||||||
|
self.__user_full_name = response_data[0:start_bracket_index-1].strip()
|
||||||
|
self.__user_email = response_data[start_bracket_index+1:end_bracket_index].strip()
|
||||||
|
else:
|
||||||
|
#
|
||||||
|
self.__user_full_name = "N/A"
|
||||||
|
self.__user_email = response_data.strip()
|
||||||
|
# Send the user to the Home menu (HMEN) as they are not logged in
|
||||||
|
self.state = "HMEN"
|
||||||
|
self.current_status = -1
|
||||||
|
else:
|
||||||
|
# VRFY failed and we send the user back to the login
|
||||||
|
self.reset_session()
|
||||||
|
self.state = "LOGI"
|
||||||
|
self.current_status = -1
|
||||||
|
elif response_code == 500:
|
||||||
|
# VRFY failed and we send the user back to the login
|
||||||
|
self.reset_session()
|
||||||
|
self.state = "LOGI"
|
||||||
|
self.current_status = -1
|
||||||
|
|
||||||
|
elif self.state == "MAIL":
|
||||||
|
# Start an email input, we start by asking for the receipt addresses (RCPT)
|
||||||
|
if response_code == 250:
|
||||||
|
self.state = "RCPT"
|
||||||
|
self.current_status = -1
|
||||||
|
else:
|
||||||
|
self.state = "HMEN"
|
||||||
|
self.current_status = -1
|
||||||
|
|
||||||
|
elif self.state == "RCPT":
|
||||||
|
self.last_response_content.clear()
|
||||||
|
if response_code == 250:
|
||||||
|
# The address was local and added
|
||||||
|
self.last_response_content.append("Address Added.")
|
||||||
|
elif response_code == 251:
|
||||||
|
# The address was valid but not local and added
|
||||||
|
self.last_response_content.append("Address Added, but not local.")
|
||||||
|
elif response_code == 503:
|
||||||
|
# The server said we are out of sequence, reset the transaction and start over
|
||||||
|
self.last_response_content.append("There was an issue with the order of commands sent, RSET sent.")
|
||||||
|
self.state = "RSET"
|
||||||
|
self.process_command("RSET")
|
||||||
|
else:
|
||||||
|
# The address was rejected, we remove it from the clients end
|
||||||
|
last_address = self.client_mail_temp.to_address_list.pop(-1)
|
||||||
|
self.last_response_content.append(f"Address {last_address} not added for reason: " + response_data)
|
||||||
|
|
||||||
|
if self.state != "RSET":
|
||||||
|
self.state = "RCPT"
|
||||||
|
self.current_status = -1
|
||||||
|
|
||||||
|
elif self.state == "SUBJ":
|
||||||
|
# The receipts were fine and we move onto the subject, subject does not have an official "state"
|
||||||
|
# we process it fully from the client side and send it along with all of the starting DATA requests
|
||||||
|
if response_code == 354:
|
||||||
|
string_date = datetime.now()
|
||||||
|
date_formatted = string_date.strftime("%d %b %y %H:%M:%S")
|
||||||
|
# send the email headers that get used when "forwarding" to another server, for completeness
|
||||||
|
# DATA starts with 4 lines, Date, From, Subject and To as a comma seperated list
|
||||||
|
self.queue_command("Date: " + date_formatted)
|
||||||
|
self.queue_command("From: " + self.__user_full_name + " <" + self.__user_email + ">")
|
||||||
|
self.queue_command("Subject: " + self.client_mail_temp.subject)
|
||||||
|
self.queue_command("To: " + ", ".join(self.client_mail_temp.to_address_list))
|
||||||
|
# Set the state to DATA and start accepting lines from the client until the >SEND command is used
|
||||||
|
self.state = "DATA"
|
||||||
|
self.current_status = -1
|
||||||
|
else:
|
||||||
|
# The server said we are out of sequence, reset the transaction and start over
|
||||||
|
self.last_response_content.append("There was an issue with the order of commands sent, RSET sent.")
|
||||||
|
self.state = "RSET"
|
||||||
|
self.process_command("RSET")
|
||||||
|
|
||||||
|
elif self.state == "DATA":
|
||||||
|
if response_code == 250:
|
||||||
|
# The data was sent and the server responded with an OK response, return to the home menu
|
||||||
|
self.last_response_content.clear()
|
||||||
|
self.last_response_content.append("Email sent.")
|
||||||
|
self.client_mail_temp = None
|
||||||
|
self.state = "HMEN"
|
||||||
|
self.current_status = -1
|
||||||
|
|
||||||
|
elif self.state == "RSET":
|
||||||
|
if response_code == 250:
|
||||||
|
# The server responded OK to our request to reset the current transaction
|
||||||
|
self.client_mail_temp = None
|
||||||
|
self.state = "HMEN"
|
||||||
|
self.current_status = -1
|
||||||
|
|
||||||
|
elif self.state == "VIEW":
|
||||||
|
if response_code == 211:
|
||||||
|
# Server responded to the VIEW command
|
||||||
|
self.current_status = 0
|
||||||
|
self.last_response_content.clear()
|
||||||
|
elif response_code == 214:
|
||||||
|
# Process the VIEW response until we get the end of DATA string
|
||||||
|
if response_data == "\n.\n":
|
||||||
|
self.current_status = -1
|
||||||
|
else:
|
||||||
|
self.last_response_content.append(response_data)
|
||||||
|
elif response_code == 504:
|
||||||
|
# There was an error getting a mail, usually if they try to enter
|
||||||
|
# a number that doesnt belong to them
|
||||||
|
self.last_response_content.clear()
|
||||||
|
self.last_response_content.append("There was an issue loading this mail")
|
||||||
|
self.current_status = -1
|
||||||
|
else:
|
||||||
|
# There was an unspecified error so we send the user back to the home menu
|
||||||
|
self.last_response_content.clear()
|
||||||
|
self.state = "HMEN"
|
||||||
|
self.current_status = -1
|
||||||
|
|
||||||
|
elif self.state == "ADMN":
|
||||||
|
if response_code == 250:
|
||||||
|
# Server has left ADMIN mode
|
||||||
|
if response_data == "OK QUIT":
|
||||||
|
self.last_response_content.clear()
|
||||||
|
self.state = "HMEN"
|
||||||
|
self.current_status = -1
|
||||||
|
# Server has processed the command given
|
||||||
|
elif response_data == "OK DONE":
|
||||||
|
self.last_response_content.clear()
|
||||||
|
self.last_response_content.append("Action Completed.")
|
||||||
|
self.current_status = -1
|
||||||
|
else:
|
||||||
|
self.last_response_content.clear()
|
||||||
|
self.current_status = -1
|
||||||
|
# Server sent a multipart ADMIN response
|
||||||
|
elif response_code == 211:
|
||||||
|
self.current_status = 0
|
||||||
|
self.last_response_content.clear()
|
||||||
|
elif response_code == 214:
|
||||||
|
if response_data == "\n.\n":
|
||||||
|
self.current_status = -1
|
||||||
|
else:
|
||||||
|
self.last_response_content.append(response_data)
|
||||||
|
# Server sent an ADMIN error response
|
||||||
|
elif response_code == 500:
|
||||||
|
self.last_response_content.clear()
|
||||||
|
self.last_response_content.append("Action Failed.")
|
||||||
|
self.current_status = -1
|
||||||
|
# Permission was denied by the server
|
||||||
|
else:
|
||||||
|
self.last_response_content.clear()
|
||||||
|
self.last_response_content.append("Permission Denied.")
|
||||||
|
self.state = "HMEN"
|
||||||
|
self.current_status = -1
|
||||||
|
|
||||||
|
elif self.state == "LOUT":
|
||||||
|
# Server logged the client out
|
||||||
|
if response_code == 250:
|
||||||
|
# Logged out
|
||||||
|
self.reset_session()
|
||||||
|
self.state = "LOGI"
|
||||||
|
else:
|
||||||
|
# Couldn't log out?
|
||||||
|
self.state = "HMEN"
|
||||||
|
|
||||||
|
self.current_status = -1
|
||||||
|
elif self.state == "HELP":
|
||||||
|
# Server responded to the HELP command and is sending back the multipart response
|
||||||
|
if response_code == 211:
|
||||||
|
self.current_status = 0
|
||||||
|
self.last_response_content.clear()
|
||||||
|
elif response_code == 214:
|
||||||
|
if response_data == "\n.\n":
|
||||||
|
self.current_status = -1
|
||||||
|
else:
|
||||||
|
self.last_response_content.append(response_data)
|
||||||
|
# Server responded with a command not implemented error becuase the help section does not exist
|
||||||
|
elif response_code == 504:
|
||||||
|
self.last_response_content.clear()
|
||||||
|
self.last_response_content.append("There was no help on file for your request.")
|
||||||
|
self.current_status = -1
|
||||||
|
# Unspecified error from the server, just sending the user to the home menu
|
||||||
|
else:
|
||||||
|
self.last_response_content.clear()
|
||||||
|
self.state = "HMEN"
|
||||||
|
self.current_status = -1
|
||||||
|
|
||||||
|
# Status 421 and 221 is only used when the connection has been terminated and we just exit at that point
|
||||||
|
if response_code == 421:
|
||||||
|
print("The connection was closed by the server with the following message:")
|
||||||
|
for line in self.last_response_content:
|
||||||
|
print(line)
|
||||||
|
else:
|
||||||
|
print(f"Reason: {response_data}")
|
||||||
|
self.__close()
|
||||||
|
self.current_status = -2
|
||||||
|
|
||||||
|
if response_code == 221:
|
||||||
|
self.state = "QUIT"
|
||||||
|
self.__close()
|
||||||
|
self.current_status = -2
|
||||||
|
|
||||||
|
else:
|
||||||
|
# The response did not have a valid response code
|
||||||
|
print("Response too short.")
|
||||||
|
|
||||||
|
def __close(self):
|
||||||
|
"""Private method to close the connection to the server and end the thread"""
|
||||||
|
print("Attempting to end the Connection...")
|
||||||
|
try:
|
||||||
|
self.__selector.unregister(self.__socket)
|
||||||
|
self.__socket.close()
|
||||||
|
print(f"Connection ended with {repr(self.__address)}")
|
||||||
|
except OSError as ex:
|
||||||
|
print(f"Could not close the connection: {repr(ex)}")
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
self.__socket = None
|
||||||
|
self.active = False
|
||||||
|
self.current_status = -1
|
||||||
|
|
||||||
|
def get_session_ref(self) -> str:
|
||||||
|
"""Method to return the current session reference from the server"""
|
||||||
|
return self.__session_ref
|
||||||
|
|
||||||
|
def get_user_name(self) -> str:
|
||||||
|
"""Method to return the current users full name"""
|
||||||
|
return self.__user_full_name
|
||||||
|
|
||||||
|
def get_user_email(self) -> str:
|
||||||
|
"""Method to return the current users email address"""
|
||||||
|
return self.__user_email
|
||||||
|
|
||||||
|
def reset_session(self) -> None:
|
||||||
|
"""Method to restart a clients session without closing the connection, used for the logout command"""
|
||||||
|
self.__session_ref = None
|
||||||
|
self.__user_email = None
|
||||||
|
self.__user_full_name = None
|
||||||
|
self.__user_role = None
|
||||||
|
self.login_username = None
|
284
Client/Crypto.py
Normal file
284
Client/Crypto.py
Normal file
@ -0,0 +1,284 @@
|
|||||||
|
# System Imports
|
||||||
|
import math
|
||||||
|
import random
|
||||||
|
|
||||||
|
# Project Imports
|
||||||
|
import SHA256Custom
|
||||||
|
|
||||||
|
|
||||||
|
class Receiver:
|
||||||
|
"""Class for dealing with the receiving end of the custom simple RSA encryption"""
|
||||||
|
def __init__(self):
|
||||||
|
# A list of small prime numbers we can use to calculate a larger one
|
||||||
|
self.__small_primes_list = [211, 223, 227, 229, 233, 239, 241, 251, 257, 263, 269, 271, 277, 281, 283, 293,
|
||||||
|
307, 311, 313, 317, 331, 337, 347, 349]
|
||||||
|
|
||||||
|
self.__p = None
|
||||||
|
self.__q = None
|
||||||
|
self.__n = None
|
||||||
|
self.__t = None
|
||||||
|
|
||||||
|
# This is apparently the most commonly used shared co-prime used for RSA
|
||||||
|
self.__e = 65537
|
||||||
|
self.__d = None
|
||||||
|
|
||||||
|
# The generated public and private keys
|
||||||
|
self.__public_key = None
|
||||||
|
self.__private_key = None
|
||||||
|
self.generated = False
|
||||||
|
|
||||||
|
# The bit size of the key
|
||||||
|
self.__bit_size = 1024
|
||||||
|
|
||||||
|
def generate_keys(self) -> bool:
|
||||||
|
"""Method to generate the RSA Public and Private keys for encryption"""
|
||||||
|
try:
|
||||||
|
# Only generate keys if we haven't already
|
||||||
|
if not self.generated:
|
||||||
|
self.__p = self.__generate_prime_number()
|
||||||
|
self.__q = self.__generate_prime_number()
|
||||||
|
|
||||||
|
# Make sure we have 2 different prime numbers
|
||||||
|
while self.__q == self.__p:
|
||||||
|
self.__q = self.__generate_prime_number()
|
||||||
|
|
||||||
|
# Most of the following is just the pseudo code for RSA prime generation
|
||||||
|
self.__n = self.__p * self.__q
|
||||||
|
self.__t = (self.__p - 1) * (self.__q - 1)
|
||||||
|
|
||||||
|
while math.gcd(self.__t, self.__e) > 1:
|
||||||
|
self.__e += 2
|
||||||
|
|
||||||
|
if self.__e < 1 or self.__e > self.__t:
|
||||||
|
raise Exception("E must be > 1 and < T")
|
||||||
|
|
||||||
|
if self.__t % self.__e == 0:
|
||||||
|
raise Exception("E is not a co-prime with T")
|
||||||
|
|
||||||
|
self.__d = Receiver.find_inverse(self.__e, self.__t)
|
||||||
|
|
||||||
|
# We have our keys, set up the tuples and flag the generated as true
|
||||||
|
self.__public_key = (self.__e, self.__n)
|
||||||
|
self.__private_key = (self.__d, self.__n)
|
||||||
|
self.generated = True
|
||||||
|
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
raise Exception("Keys have already been generated.")
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_expected_block_size(self) -> int:
|
||||||
|
"""Method to return the expected block size for the encrypted data"""
|
||||||
|
# the encrypted request blocksize seems to be half of the bit size
|
||||||
|
return int(self.__bit_size / 2)
|
||||||
|
|
||||||
|
def get_public_key_pair(self, hex_result=True):
|
||||||
|
"""Method for returning the public key as a hex string or tuple depending on the hex_results"""
|
||||||
|
if hex_result:
|
||||||
|
# Return both public key elements as a hex string
|
||||||
|
return "{0:x} {1:x}".format(self.__public_key[0], self.__public_key[1])
|
||||||
|
else:
|
||||||
|
return self.__public_key
|
||||||
|
|
||||||
|
def get_private_key_pair(self, hex_result=True):
|
||||||
|
"""Method for returning the private key as a hex string or tuple, not really used"""
|
||||||
|
if hex_result:
|
||||||
|
# Return both private key elements as a hex string
|
||||||
|
return "{0:x} {1:x}".format(self.__private_key[0], self.__private_key[1])
|
||||||
|
else:
|
||||||
|
return self.__private_key
|
||||||
|
|
||||||
|
def decrypt_string(self, string_in: str) -> str:
|
||||||
|
"""Method for decrypting our custom RSA encrypted blocks, takes the hex string in"""
|
||||||
|
if self.generated:
|
||||||
|
# Convert the hex string back into an int
|
||||||
|
encrypted_string_as_int = int(string_in, 16)
|
||||||
|
# Do the math on our encrypted int
|
||||||
|
decrypted_string_as_int = pow(encrypted_string_as_int, self.__d, self.__n)
|
||||||
|
# Convert the result back into a hex string
|
||||||
|
decrypted_string_as_hex = "{0:x}".format(decrypted_string_as_int)
|
||||||
|
|
||||||
|
start_padding_included = False
|
||||||
|
plain_text_read = False
|
||||||
|
|
||||||
|
plain_text_string = ''
|
||||||
|
hashed_checksum = ''
|
||||||
|
|
||||||
|
# Loop for every 2 characters (hex)
|
||||||
|
for i in range(0, len(decrypted_string_as_hex), 2):
|
||||||
|
current_byte = decrypted_string_as_hex[i:i+2]
|
||||||
|
# the encrypted string should start with a padding FF value
|
||||||
|
if not start_padding_included and current_byte.upper() == "FF":
|
||||||
|
start_padding_included = True
|
||||||
|
# the Checksum hash should start after the 00 value
|
||||||
|
elif not plain_text_read and start_padding_included and current_byte == "00":
|
||||||
|
plain_text_read = True
|
||||||
|
# Everything else should be the message or the checksum hash
|
||||||
|
elif start_padding_included:
|
||||||
|
if not plain_text_read:
|
||||||
|
# The message is padded after the fact to fill up the bit size, these are ignored
|
||||||
|
if current_byte.upper() != "FF":
|
||||||
|
# Convert the hex value back into a utf character
|
||||||
|
plain_text_string += chr(int(current_byte, 16))
|
||||||
|
else:
|
||||||
|
# Reconstruct the hash checksum
|
||||||
|
hashed_checksum += current_byte
|
||||||
|
|
||||||
|
# If the text of hash is missing then something went wrong with encrypting or sending
|
||||||
|
if len(plain_text_string) == 0 or len(hashed_checksum) == 0:
|
||||||
|
raise Exception("No text was decrypted, something must be wrong...")
|
||||||
|
|
||||||
|
# Hash the plain text string and make sure it matches our checksum hash
|
||||||
|
hash_test = SHA256Custom.SHA256Custom()
|
||||||
|
hash_test.update(plain_text_string.encode())
|
||||||
|
hashed_plain_text = hash_test.hexdigest()
|
||||||
|
|
||||||
|
if hashed_plain_text == hashed_checksum:
|
||||||
|
# The message has not been altered, decrypted successfully
|
||||||
|
return plain_text_string
|
||||||
|
else:
|
||||||
|
raise Exception("Plain text did not match the Hashed Checksum, something is wrong.")
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise Exception("No key has been generated")
|
||||||
|
|
||||||
|
def __generate_prime_number(self) -> int:
|
||||||
|
"""Private method for generating prime numbers with the bit size set"""
|
||||||
|
while True:
|
||||||
|
current_prime_candidate = self.__generate_low_level_prime(self.__bit_size)
|
||||||
|
if not self.miller_rabin_prime_check(current_prime_candidate):
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
return current_prime_candidate
|
||||||
|
|
||||||
|
def __generate_low_level_prime(self, prime_candidate: int) -> int:
|
||||||
|
"""Private method for generating the prime number using the candidate number and the small prime array"""
|
||||||
|
while True:
|
||||||
|
current_prime_candidate = self.generate_random_large_int(prime_candidate)
|
||||||
|
|
||||||
|
# We try to generate a large prime number using the candidate and looping through the small primes
|
||||||
|
# array until we get one
|
||||||
|
for divisor in self.__small_primes_list:
|
||||||
|
if current_prime_candidate % divisor == 0 and divisor ** 2 <= current_prime_candidate:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
return current_prime_candidate
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def generate_random_large_int(n_bit_size: int) -> int:
|
||||||
|
"""Static method to generate a large int value"""
|
||||||
|
# Get a random number in a range of the bit size given
|
||||||
|
return random.randrange(2 ** (n_bit_size - 1) + 1, 2 ** n_bit_size - 1)
|
||||||
|
|
||||||
|
# Method to test that the prime number we generate passes the miller rabin prime candidate test
|
||||||
|
@staticmethod
|
||||||
|
def miller_rabin_prime_check(prime_candidate: int) -> bool:
|
||||||
|
"""Static method for the miller rabin prime number checking algorithm, to ensure that we have a valid prime"""
|
||||||
|
max_divisions_by_two = 0
|
||||||
|
prime_minus_one = prime_candidate - 1
|
||||||
|
while prime_minus_one % 2 == 0:
|
||||||
|
prime_minus_one >>= 1
|
||||||
|
max_divisions_by_two += 1
|
||||||
|
|
||||||
|
assert 2 ** max_divisions_by_two * prime_minus_one == prime_candidate - 1
|
||||||
|
|
||||||
|
def trial_composite(round_tester_in):
|
||||||
|
"""Sub method for doing the actual prime number factorisation check"""
|
||||||
|
if pow(round_tester_in, prime_minus_one, prime_candidate) == 1:
|
||||||
|
return False
|
||||||
|
|
||||||
|
for x in range(max_divisions_by_two):
|
||||||
|
if pow(round_tester_in, 2 ** x * prime_minus_one, prime_candidate) == prime_candidate - 1:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
for i in range(0, 20):
|
||||||
|
round_tester = random.randrange(2, prime_candidate)
|
||||||
|
if trial_composite(round_tester):
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def extended_euclidean_algorithm(a: int, b: int) -> tuple:
|
||||||
|
"""Static method for the EEA from the pseudo code"""
|
||||||
|
if b == 0:
|
||||||
|
return 1, 0
|
||||||
|
|
||||||
|
q = a // b
|
||||||
|
r = a % b
|
||||||
|
s, t = Receiver.extended_euclidean_algorithm(b, r)
|
||||||
|
return t, s-(q*t)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def find_inverse(a: int, b: int) -> int:
|
||||||
|
"""Static method for finding the inverse of the primes given"""
|
||||||
|
inverse = Receiver.extended_euclidean_algorithm(a, b)[0]
|
||||||
|
if inverse < 1:
|
||||||
|
inverse += b
|
||||||
|
|
||||||
|
return inverse
|
||||||
|
|
||||||
|
|
||||||
|
class Transmitter:
|
||||||
|
"""Class for dealing with the transmitting end of the custom simple RSA encryption"""
|
||||||
|
def __init__(self, public_key: str):
|
||||||
|
# We should be getting the string hex values from the receiver and splitting them into the actual parts
|
||||||
|
public_key_parts = public_key.split(" ", 1)
|
||||||
|
|
||||||
|
# Must match the Receiver bit size
|
||||||
|
self.__bit_size = 1024
|
||||||
|
|
||||||
|
# Check if the public key has 2 parts to be valid
|
||||||
|
if len(public_key_parts) == 2:
|
||||||
|
self.__e = int(public_key_parts[0], 16)
|
||||||
|
self.__n = int(public_key_parts[1], 16)
|
||||||
|
self.__has_key = True
|
||||||
|
else:
|
||||||
|
self.__has_key = False
|
||||||
|
raise Exception("Public key was incorrect")
|
||||||
|
|
||||||
|
def get_current_key_pair(self):
|
||||||
|
"""Method for getting the key pair values e and n"""
|
||||||
|
return self.__e, self.__n
|
||||||
|
|
||||||
|
def get_usable_byte_length(self) -> int:
|
||||||
|
"""Method for getting the usable byte length incase we need to split a request into multiple parts"""
|
||||||
|
# Im not entirely sure on this as it was calculated mostly by "brute force" but it seems to be the
|
||||||
|
# key size divided by 4
|
||||||
|
# minus 2 bytes for the start and hash break bytes added
|
||||||
|
# minus 32 for the SHA256 hash size
|
||||||
|
# minus 32 for the RSA byte overhead, i've tried different values but getting too close to 256 bytes will
|
||||||
|
# sometimes case issues which i think is down to
|
||||||
|
return int(self.__bit_size / 4) - 2 - 32 - 32
|
||||||
|
|
||||||
|
def encrypt_string(self, string_in: bytes) -> bytes:
|
||||||
|
"""Method for encrypting a string using the public RSA key provided by the receiver"""
|
||||||
|
if self.__has_key:
|
||||||
|
# Create a hash of the message given for our checksum
|
||||||
|
string_hashing = SHA256Custom.SHA256Custom()
|
||||||
|
string_hashing.update(string_in)
|
||||||
|
|
||||||
|
string_hashed = string_hashing.hexdigest()
|
||||||
|
|
||||||
|
# Pad to make the string the full size of the custom RSA block
|
||||||
|
padding = ""
|
||||||
|
if len(string_in) < self.get_usable_byte_length():
|
||||||
|
padding_required = self.get_usable_byte_length() - len(string_in) - 1
|
||||||
|
padding = "ff" * padding_required
|
||||||
|
|
||||||
|
# Add an FF pad at the start of the string, since if the input hex starts with a 0X value the 0 is dropped
|
||||||
|
# when it is converted to an int, im not entirely sure how they get around this
|
||||||
|
hex_string = "ff" + string_in.hex() + padding + "00" + string_hashed
|
||||||
|
string_as_int = int(hex_string, 16)
|
||||||
|
|
||||||
|
# Do the math to the int value
|
||||||
|
encrypted_string_as_int = pow(string_as_int, self.__e, self.__n)
|
||||||
|
# Convert the int value into a hex string
|
||||||
|
encrypted_string_hex = "{0:x}".format(encrypted_string_as_int)
|
||||||
|
|
||||||
|
return encrypted_string_hex.encode()
|
||||||
|
else:
|
||||||
|
raise Exception("No key has been generated")
|
126
Client/SHA256Custom.py
Normal file
126
Client/SHA256Custom.py
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
class SHA256Custom:
|
||||||
|
"""
|
||||||
|
Custom SHA256 function, its not really custom but rewritten using the pseudocode from wikipedia and
|
||||||
|
should mimic the built in function
|
||||||
|
"""
|
||||||
|
def __init__(self, data=None):
|
||||||
|
# Setting up the initial bit values, i wont pretend to understand the math involved but these seem hardcoded
|
||||||
|
self.__ks = [0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
|
||||||
|
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
|
||||||
|
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
||||||
|
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
|
||||||
|
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
|
||||||
|
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
||||||
|
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
|
||||||
|
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2]
|
||||||
|
|
||||||
|
self.__hs = [0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19]
|
||||||
|
|
||||||
|
# Copy the initial bit values for use since they are changed
|
||||||
|
self.__h = self.__hs[:]
|
||||||
|
self.__k = self.__ks[:]
|
||||||
|
|
||||||
|
# Create variables we will need in the class
|
||||||
|
self.__string_byte_len = 0
|
||||||
|
self.__buffer = b''
|
||||||
|
self.__string_hashed = False
|
||||||
|
self.__digested_string = b''
|
||||||
|
|
||||||
|
if data is not None:
|
||||||
|
self.update(data)
|
||||||
|
|
||||||
|
# Static methods because pycharm is moaning about them not needing to be inside of the class
|
||||||
|
@staticmethod
|
||||||
|
def pad_string(string_len: int) -> bytes:
|
||||||
|
"""Static method to pad the string digest for the hash"""
|
||||||
|
# Adding a "1" bit to the end, although it seems to use 63 as the actual value, im guessing its possibly to do
|
||||||
|
# with chunk size
|
||||||
|
string_di = string_len & 0x3f
|
||||||
|
# Get the 64bit big endian length of the post processed hash length
|
||||||
|
post_process_length = (string_len << 3).to_bytes(8, "big")
|
||||||
|
padded_length = 55
|
||||||
|
if string_di < 56:
|
||||||
|
padded_length -= string_di
|
||||||
|
else:
|
||||||
|
padded_length -= 199 - string_di
|
||||||
|
|
||||||
|
return b'\x80' + b'\x00' * padded_length + post_process_length
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def rotate_right(a, b):
|
||||||
|
"""Static method to rotate a and b bits right """
|
||||||
|
return ((a >> b) | (a << (32 - b))) & 0xffffffff
|
||||||
|
|
||||||
|
def update(self, string_in: bytes) -> None:
|
||||||
|
"""Method to update the hash value"""
|
||||||
|
if (string_in and len(string_in) > 0) and not self.__string_hashed:
|
||||||
|
self.__string_byte_len += len(string_in)
|
||||||
|
string_buffer = self.__buffer + string_in
|
||||||
|
|
||||||
|
# Process the string as 512 bit chunks (64 bytes)
|
||||||
|
for i in range(0, len(string_buffer) // 64):
|
||||||
|
self.calculate_hash(string_buffer[64 * i:64 * (i + 1)])
|
||||||
|
|
||||||
|
# Set the class buffer
|
||||||
|
self.__buffer = string_buffer[len(string_buffer) - (len(string_buffer) % 64):]
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
|
||||||
|
def digest(self) -> bytes:
|
||||||
|
"""Method to get the bytes digest of our hash value"""
|
||||||
|
if not self.__string_hashed:
|
||||||
|
self.update(self.pad_string(self.__string_byte_len))
|
||||||
|
self.__digested_string = b''.join(val.to_bytes(4, 'big') for val in self.__h[:8])
|
||||||
|
self.__string_hashed = True
|
||||||
|
return self.__digested_string
|
||||||
|
|
||||||
|
def hexdigest(self) -> str:
|
||||||
|
"""Method to get the hex digest of our hash value"""
|
||||||
|
# Hex characters to use when changing the hex value back into a string
|
||||||
|
tab = '0123456789abcdef'
|
||||||
|
return "".join(tab[byt >> 4] + tab[byt & 0xf] for byt in self.digest())
|
||||||
|
|
||||||
|
def calculate_hash(self, chunk) -> None:
|
||||||
|
"""Method to calculate the actual hash value"""
|
||||||
|
# Create an array of 32bit words
|
||||||
|
words = [0] * 64
|
||||||
|
|
||||||
|
# Converted from the pseudo code, but i believe its filling the word array with first 16 words we add
|
||||||
|
words[0:16] = [int.from_bytes(chunk[i:i + 4], "big") for i in range(0, len(chunk), 4)]
|
||||||
|
|
||||||
|
for i in range(16, 64):
|
||||||
|
s0 = self.rotate_right(words[i - 15], 7) ^ self.rotate_right(words[i - 15], 18) ^ (words[i - 15] >> 3)
|
||||||
|
s1 = self.rotate_right(words[i - 2], 17) ^ self.rotate_right(words[i - 2], 19) ^ (words[i - 2] >> 10)
|
||||||
|
words[i] = (words[i - 16] + s0 + words[i - 7] + s1) & 0xffffffff
|
||||||
|
|
||||||
|
# Initialize the values with the default hash value
|
||||||
|
a, b, c, d, e, f, g, h = self.__h
|
||||||
|
|
||||||
|
# Calculation main loop
|
||||||
|
for i in range(64):
|
||||||
|
s0 = self.rotate_right(a, 2) ^ self.rotate_right(a, 13) ^ self.rotate_right(a, 22)
|
||||||
|
t2 = s0 + self.maj(a, b, c)
|
||||||
|
s1 = self.rotate_right(e, 6) ^ self.rotate_right(e, 11) ^ self.rotate_right(e, 25)
|
||||||
|
t1 = h + s1 + self.ch(e, f, g) + self.__k[i] + words[i]
|
||||||
|
|
||||||
|
h = g
|
||||||
|
g = f
|
||||||
|
f = e
|
||||||
|
e = (d + t1) & 0xffffffff
|
||||||
|
d = c
|
||||||
|
c = b
|
||||||
|
b = a
|
||||||
|
a = (t1 + t2) & 0xffffffff
|
||||||
|
|
||||||
|
for i, (x, y) in enumerate(zip(self.__h, [a, b, c, d, e, f, g, h])):
|
||||||
|
self.__h[i] = (x + y) & 0xffffffff
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def maj(a, b, c):
|
||||||
|
"""Static method for the calculation of the maj variable in the pseudo code"""
|
||||||
|
return (a & b) ^ (a & c) ^ (b & c)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def ch(a, b, c):
|
||||||
|
"""Static method to calculate the ch variable in the pseudo code"""
|
||||||
|
return (a & b) ^ ((~a) & c)
|
243
Client/SMTPClient.py
Normal file
243
Client/SMTPClient.py
Normal file
@ -0,0 +1,243 @@
|
|||||||
|
import socket
|
||||||
|
|
||||||
|
import ClientTempMail
|
||||||
|
from ClientThread import ClientThread
|
||||||
|
|
||||||
|
|
||||||
|
class SMTPClient:
|
||||||
|
"""Class to hold the methods needed to process the client side of the script"""
|
||||||
|
def __init__(self, host: str, port: int):
|
||||||
|
self.__host = host
|
||||||
|
self.__port = port
|
||||||
|
|
||||||
|
self.__client_thread: ClientThread or None = None
|
||||||
|
|
||||||
|
def is_active(self) -> bool:
|
||||||
|
"""Method to check if the child thread is still active"""
|
||||||
|
return self.__client_thread.active
|
||||||
|
|
||||||
|
def is_connected(self) -> bool:
|
||||||
|
"""Method to check if the child thread is still connected"""
|
||||||
|
try:
|
||||||
|
return self.__client_thread.is_connected()
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_state(self) -> str:
|
||||||
|
"""Method to return the child threads state"""
|
||||||
|
return self.__client_thread.state
|
||||||
|
|
||||||
|
def get_current_response_content(self) -> any:
|
||||||
|
"""Method to return the client threads last response array"""
|
||||||
|
return self.__client_thread.last_response_content
|
||||||
|
|
||||||
|
def get_current_login_username(self) -> str:
|
||||||
|
"""Method to return the current logged in users username"""
|
||||||
|
return self.__client_thread.login_username
|
||||||
|
|
||||||
|
def get_current_users_name(self) -> str:
|
||||||
|
"""Method to return the current users full name"""
|
||||||
|
return self.__client_thread.get_user_name()
|
||||||
|
|
||||||
|
def get_current_users_email_address(self) -> str:
|
||||||
|
"""Method to return the current users email address"""
|
||||||
|
return self.__client_thread.get_user_email()
|
||||||
|
|
||||||
|
def get_current_temp_mail(self) -> ClientTempMail.ClientTempMail:
|
||||||
|
"""Method to return the current temp mail class object from the child thread"""
|
||||||
|
return self.__client_thread.client_mail_temp
|
||||||
|
|
||||||
|
def current_status(self) -> int:
|
||||||
|
"""Method to return the child threads current status"""
|
||||||
|
return self.__client_thread.current_status
|
||||||
|
|
||||||
|
def get_users_role(self) -> str:
|
||||||
|
"""Method to return the current users role"""
|
||||||
|
return self.__client_thread.get_users_role()
|
||||||
|
|
||||||
|
def start_connection(self) -> bool:
|
||||||
|
"""Method to start the clients connection to the server"""
|
||||||
|
server_address = (self.__host, self.__port)
|
||||||
|
|
||||||
|
current_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
current_socket.setblocking(False)
|
||||||
|
current_socket.connect_ex(server_address)
|
||||||
|
|
||||||
|
result = False
|
||||||
|
try:
|
||||||
|
# Create the child thread for the connection
|
||||||
|
self.__client_thread = ClientThread(current_socket, server_address)
|
||||||
|
self.__client_thread.start()
|
||||||
|
|
||||||
|
self.__client_thread.current_status = 0
|
||||||
|
result = True
|
||||||
|
except OSError as ex:
|
||||||
|
print(f"Error connecting to the server: {repr(ex)}")
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def close_connection(self) -> None:
|
||||||
|
"""Method to close the client connection"""
|
||||||
|
print("Attempting to end the Connection...")
|
||||||
|
try:
|
||||||
|
self.__client_thread.close_connection()
|
||||||
|
print(f"Connection ended with {self.__host}:{self.__port}")
|
||||||
|
except OSError as ex:
|
||||||
|
print(f"Could not close the connection: {repr(ex)}")
|
||||||
|
finally:
|
||||||
|
self.__client_thread = None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def email_string_sanitize(input_str: str):
|
||||||
|
"""Static method to sanitize an email address"""
|
||||||
|
# Allowable Characters from !#$%&'*+-/=?^_`{|}~@.
|
||||||
|
special_chars = [33, 35, 36, 37, 38, 39, 42, 43, 45, 46, 47, 61, 63, 64, 94, 95, 96, 123, 124, 125, 126]
|
||||||
|
|
||||||
|
output_str = ""
|
||||||
|
for letter in input_str:
|
||||||
|
if (letter in special_chars) or (48 <= ord(letter) < 58) \
|
||||||
|
or (65 <= ord(letter) < 91) or (97 <= ord(letter) < 122):
|
||||||
|
output_str += letter
|
||||||
|
|
||||||
|
return output_str
|
||||||
|
|
||||||
|
def run_command(self, command: str, options=None) -> bool:
|
||||||
|
"""Method that processes the commands as they're sent"""
|
||||||
|
valid_command = False
|
||||||
|
if command is not None:
|
||||||
|
valid_command = True
|
||||||
|
|
||||||
|
full_command = None
|
||||||
|
# Login command processing
|
||||||
|
if command == "LOGI":
|
||||||
|
if options['username'] is not None:
|
||||||
|
# Clean the username to only accept email safe characters
|
||||||
|
cleaned_username = self.email_string_sanitize(options['username'])
|
||||||
|
else:
|
||||||
|
cleaned_username = ""
|
||||||
|
valid_command = False
|
||||||
|
|
||||||
|
password = options['password']
|
||||||
|
|
||||||
|
# Check if the username and password have been set correctly
|
||||||
|
if len(cleaned_username) > 0 and len(password) > 0:
|
||||||
|
self.__client_thread.login_username = cleaned_username
|
||||||
|
full_command = "LOGI " + cleaned_username + " " + password
|
||||||
|
else:
|
||||||
|
valid_command = False
|
||||||
|
|
||||||
|
# Start the mail command
|
||||||
|
elif command == "MAIL":
|
||||||
|
self.__client_thread.state = "MAIL"
|
||||||
|
# Create a mail object to hold the temp values we send to the server
|
||||||
|
self.__client_thread.client_mail_temp = ClientTempMail.ClientTempMail()
|
||||||
|
self.__client_thread.client_mail_temp.from_address = self.__client_thread.get_user_email
|
||||||
|
# Send the initial MAIL FROM command
|
||||||
|
full_command = "MAIL FROM <" + self.__client_thread.get_user_email() + ">"
|
||||||
|
|
||||||
|
elif command == "RCPT":
|
||||||
|
# Strip any whitespaces to stop it bypassing the len check
|
||||||
|
to_address = options['to_address'].strip()
|
||||||
|
if len(to_address) > 0:
|
||||||
|
# Check if the address is already in the list
|
||||||
|
if to_address not in self.__client_thread.client_mail_temp.to_address_list:
|
||||||
|
# Add the to address to the list and try to process it on the server
|
||||||
|
self.__client_thread.client_mail_temp.to_address_list.append(to_address)
|
||||||
|
full_command = "RCPT TO <" + to_address + ">"
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
elif command == "SUBJ":
|
||||||
|
# Check if the user has entered at least 1 address
|
||||||
|
if len(self.__client_thread.client_mail_temp.to_address_list) > 0:
|
||||||
|
self.__client_thread.state = "SUBJ"
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
elif command == "DATA":
|
||||||
|
# Set the Subject locally because SUBJ is not an actual command in the mail sequence
|
||||||
|
self.__client_thread.client_mail_temp.subject = options['subject']
|
||||||
|
# Start the local data input
|
||||||
|
full_command = "DATA"
|
||||||
|
|
||||||
|
elif command == "LINE":
|
||||||
|
# Send the current data line to the server to append to the body
|
||||||
|
self.__client_thread.queue_command(options['data_line'])
|
||||||
|
return True
|
||||||
|
|
||||||
|
elif command == "SEND":
|
||||||
|
# Finish composing the email and send the end line
|
||||||
|
self.__client_thread.current_status = 0
|
||||||
|
self.__client_thread.process_command(options['data_line'])
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Send the reset command
|
||||||
|
elif command == "RSET":
|
||||||
|
self.__client_thread.current_status = 0
|
||||||
|
self.__client_thread.state = "RSET"
|
||||||
|
full_command = "RSET"
|
||||||
|
|
||||||
|
# Send the view request
|
||||||
|
elif command == "VIEW":
|
||||||
|
if options and options['mail_id']:
|
||||||
|
# Client is trying to quit the view loop
|
||||||
|
if options['mail_id'].upper() == ">QUIT":
|
||||||
|
full_command = "VIEW QUIT"
|
||||||
|
# Client is trying to view the entire mailbox lis
|
||||||
|
elif options['mail_id'].upper() == ">LIST":
|
||||||
|
full_command = "VIEW LIST"
|
||||||
|
else:
|
||||||
|
# Client is trying to view a specific mail
|
||||||
|
full_command = "VIEW MAIL " + options['mail_id']
|
||||||
|
else:
|
||||||
|
# Do the original view list when the view option has been chosen
|
||||||
|
self.__client_thread.state = "VIEW"
|
||||||
|
full_command = "VIEW LIST"
|
||||||
|
|
||||||
|
# Send the logout command
|
||||||
|
elif command == "LOUT":
|
||||||
|
self.__client_thread.state = "LOUT"
|
||||||
|
full_command = "LOUT"
|
||||||
|
|
||||||
|
# send the help command
|
||||||
|
elif command == "HELP":
|
||||||
|
self.__client_thread.state = "HELP"
|
||||||
|
full_command = "HELP"
|
||||||
|
# Check if the user has entered a help section to search
|
||||||
|
if options and options['help_section']:
|
||||||
|
# The client wants to quit
|
||||||
|
if options['help_section'].upper() == ">QUIT":
|
||||||
|
full_command += " QUIT"
|
||||||
|
else:
|
||||||
|
full_command += " " + options['help_section'].upper()
|
||||||
|
|
||||||
|
# Send the ADMN request for admin users
|
||||||
|
elif command == "ADMN":
|
||||||
|
self.__client_thread.state = "ADMN"
|
||||||
|
if options and options['admin_command']:
|
||||||
|
# Do an unlock command to unlock a locked out user
|
||||||
|
if options['admin_command'].upper().startswith(">UNLK"):
|
||||||
|
unlock_command_parts = options['admin_command'].split(" ", 1)
|
||||||
|
full_command = "ADMN UNLK " + unlock_command_parts[1]
|
||||||
|
# Do a server log audit view
|
||||||
|
elif options['admin_command'].upper().startswith(">AUDT"):
|
||||||
|
full_command = "ADMN AUDT"
|
||||||
|
# Quit the admin command menu
|
||||||
|
elif options['admin_command'].upper().startswith(">QUIT"):
|
||||||
|
full_command = "ADMN QUIT"
|
||||||
|
else:
|
||||||
|
# Get the admin home menu
|
||||||
|
full_command = "ADMN HOME"
|
||||||
|
|
||||||
|
# Command does not need special processing
|
||||||
|
else:
|
||||||
|
full_command = command
|
||||||
|
|
||||||
|
if full_command and valid_command:
|
||||||
|
self.__client_thread.process_command(full_command)
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
301
Client/client.py
Normal file
301
Client/client.py
Normal file
@ -0,0 +1,301 @@
|
|||||||
|
import SMTPClient
|
||||||
|
import socket
|
||||||
|
import time
|
||||||
|
|
||||||
|
default_host = "127.0.0.1"
|
||||||
|
default_port = 50000
|
||||||
|
host = default_host
|
||||||
|
port = default_port
|
||||||
|
quit_flag = False
|
||||||
|
# Variable to setting to print the title to sections only once
|
||||||
|
current_state_initialised = False
|
||||||
|
|
||||||
|
|
||||||
|
# Handy function to draw the title block for each section when needed
|
||||||
|
def print_title(title: str) -> None:
|
||||||
|
title_bar_char = '-'
|
||||||
|
title_bar = "".join(title_bar_char * (len(title) + 2))
|
||||||
|
print(f"+{title_bar}+\n| {title} |\n+{title_bar}+")
|
||||||
|
|
||||||
|
|
||||||
|
# Handy function for dealing with Quit commands for the script
|
||||||
|
def do_a_quit(message: str) -> bool:
|
||||||
|
print(message)
|
||||||
|
selected_option = input("[Y]es/[N]o: ").strip().upper()
|
||||||
|
|
||||||
|
if selected_option == "Y" or selected_option == "YES":
|
||||||
|
result = False
|
||||||
|
else:
|
||||||
|
# quit the program loop
|
||||||
|
result = True
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
# start main program loop
|
||||||
|
if __name__ == "__main__":
|
||||||
|
while not quit_flag:
|
||||||
|
# ask user for a hostname and port number, or leave blank to use the default values
|
||||||
|
print(f"Please enter a valid host address and port: (default: {default_host}:{default_port})")
|
||||||
|
host_input = input("host:port, blank for default:")
|
||||||
|
|
||||||
|
valid_host = True
|
||||||
|
test_host = ""
|
||||||
|
test_port = ""
|
||||||
|
|
||||||
|
# check if a value has been entered or to use the default values
|
||||||
|
if len(host_input.strip()) > 0:
|
||||||
|
# strip any trailing/leading whitespace and split by the colon
|
||||||
|
host_input = host_input.strip()
|
||||||
|
host_parts = host_input.split(':', 1)
|
||||||
|
|
||||||
|
# check if the host_parts array has 2 values, these should be a host ip and port
|
||||||
|
if len(host_parts) == 2:
|
||||||
|
try:
|
||||||
|
# convert the first item in host_parts into a valid IP address if host name is given
|
||||||
|
test_host = socket.gethostbyname(host_parts[0])
|
||||||
|
# check if the second item in host_parts is numeric for the port number
|
||||||
|
if host_parts[1].isnumeric():
|
||||||
|
test_port = int(host_parts[1])
|
||||||
|
else:
|
||||||
|
valid_host = False
|
||||||
|
except socket.gaierror:
|
||||||
|
valid_host = False
|
||||||
|
else:
|
||||||
|
valid_host = False
|
||||||
|
# if no value is given then use the default host and port given at the top
|
||||||
|
else:
|
||||||
|
# ensure the default host and port are assigned to the test values since they do not need to be
|
||||||
|
# checked/sanitized
|
||||||
|
test_host = default_host
|
||||||
|
test_port = default_port
|
||||||
|
# check if the host is still valid
|
||||||
|
if valid_host:
|
||||||
|
# set the actual host and port variables
|
||||||
|
host = test_host
|
||||||
|
port = test_port
|
||||||
|
|
||||||
|
print(f"Connecting to {host}:{port}...")
|
||||||
|
|
||||||
|
quit_input_flag = False
|
||||||
|
# Start the client with the given host and port
|
||||||
|
smtp_client = SMTPClient.SMTPClient(host, port)
|
||||||
|
|
||||||
|
# Attempt to connect 3 times before giving up
|
||||||
|
for i in range(3):
|
||||||
|
if smtp_client.start_connection():
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
print("Connection failed, attempting again in 5 seconds")
|
||||||
|
time.sleep(5)
|
||||||
|
|
||||||
|
if smtp_client.is_connected():
|
||||||
|
# The main client loop
|
||||||
|
while smtp_client.is_active() and not quit_flag:
|
||||||
|
|
||||||
|
# We wait for the current status to be in "write" mode
|
||||||
|
if smtp_client.current_status() == -1:
|
||||||
|
# Print the main menu if we are in the HMEN (home menu)
|
||||||
|
if smtp_client.get_state() == "HMEN":
|
||||||
|
if not current_state_initialised:
|
||||||
|
print_title("SMTP server home menu")
|
||||||
|
print(f"Welcome {smtp_client.get_current_users_name()}.")
|
||||||
|
print(f"Email: {smtp_client.get_current_users_email_address()}")
|
||||||
|
print("[1]: [C]ompose an Email")
|
||||||
|
print("[2]: [V]iew your Mailbox")
|
||||||
|
print("[3]: [H]elp")
|
||||||
|
print("[4]: [L]og Out")
|
||||||
|
print("[5]: [Q]uit")
|
||||||
|
# Secret admin menu option if you are on an Admin account
|
||||||
|
if smtp_client.get_users_role() == "ADMIN":
|
||||||
|
print("[6]: [A]dmin console")
|
||||||
|
current_state_initialised = True
|
||||||
|
|
||||||
|
# If we have any response messages waiting, print them
|
||||||
|
if len(smtp_client.get_current_response_content()) > 0:
|
||||||
|
print("\nLast Status Message:")
|
||||||
|
print("----------------------")
|
||||||
|
for line in smtp_client.get_current_response_content():
|
||||||
|
print(line)
|
||||||
|
|
||||||
|
current_command = input("Option: ")
|
||||||
|
if current_command in ["1", "C", "c"]:
|
||||||
|
current_state_initialised = False
|
||||||
|
smtp_client.run_command("MAIL")
|
||||||
|
elif current_command in ["2", "V", "v"]:
|
||||||
|
current_state_initialised = False
|
||||||
|
smtp_client.run_command("VIEW")
|
||||||
|
elif current_command in ["3", "H", "h"]:
|
||||||
|
current_state_initialised = False
|
||||||
|
smtp_client.run_command("HELP")
|
||||||
|
elif current_command in ["4", "L", "l"]:
|
||||||
|
current_state_initialised = False
|
||||||
|
smtp_client.run_command("LOUT")
|
||||||
|
elif current_command in ["5", "Q", "q"]:
|
||||||
|
current_state_initialised = False
|
||||||
|
smtp_client.run_command("QUIT")
|
||||||
|
elif current_command in ["6", "A", "a"] and smtp_client.get_users_role() == "ADMIN":
|
||||||
|
current_state_initialised = False
|
||||||
|
smtp_client.run_command("ADMN")
|
||||||
|
else:
|
||||||
|
print("Invalid command, try again.")
|
||||||
|
|
||||||
|
# The admin menu allows for some specific Admin commands
|
||||||
|
elif smtp_client.get_state() == "ADMN":
|
||||||
|
print_title("Secret Admin Menu")
|
||||||
|
|
||||||
|
admin_content = smtp_client.get_current_response_content()
|
||||||
|
if len(admin_content) > 0:
|
||||||
|
print("SERVER RESPONDED: ")
|
||||||
|
for admin_line in admin_content:
|
||||||
|
print(admin_line)
|
||||||
|
print("END\n")
|
||||||
|
|
||||||
|
print("Admin Commands:")
|
||||||
|
print(">UNLK 'username' - Unlock a User account.")
|
||||||
|
print(">AUDT - View last 20 server actions")
|
||||||
|
print(">QUIT - Back to the Home menu")
|
||||||
|
|
||||||
|
current_command = input("Option: ")
|
||||||
|
if len(current_command) > 0:
|
||||||
|
smtp_client.run_command("ADMN", {"admin_command": current_command})
|
||||||
|
|
||||||
|
# Login details entry
|
||||||
|
elif smtp_client.get_state() == "LOGI":
|
||||||
|
print_title("Login Details")
|
||||||
|
print("Please enter your Login details:")
|
||||||
|
username = input("Username: ")
|
||||||
|
password = input("Password: ")
|
||||||
|
login_details = {"username": username, "password": password}
|
||||||
|
if not smtp_client.run_command("LOGI", login_details):
|
||||||
|
if do_a_quit("You did not enter a valid username and/or password, try again?"):
|
||||||
|
# send the quit command to the server
|
||||||
|
smtp_client.run_command("QUIT")
|
||||||
|
quit_flag = True
|
||||||
|
|
||||||
|
# Recipient email address entry, this continues til an empty value is entered
|
||||||
|
elif smtp_client.get_state() == "RCPT":
|
||||||
|
if not current_state_initialised:
|
||||||
|
print_title("Composing an Email: Recipients")
|
||||||
|
print("Input \">QUIT\" at any point to quit back to the menu.")
|
||||||
|
print("Input a recipient Email address and press enter, "
|
||||||
|
"when you are done press enter again.")
|
||||||
|
current_state_initialised = True
|
||||||
|
|
||||||
|
current_recp_address = input("TO: ")
|
||||||
|
if len(current_recp_address) > 0:
|
||||||
|
# Quit the compose email loop and send the RSET command
|
||||||
|
if current_recp_address.upper() == ">QUIT":
|
||||||
|
current_state_initialised = False
|
||||||
|
smtp_client.run_command("RSET")
|
||||||
|
else:
|
||||||
|
if not smtp_client.run_command("RCPT", {'to_address': current_recp_address}):
|
||||||
|
print("You must enter a unique recipient address.")
|
||||||
|
else:
|
||||||
|
# Continue to the Subject entry state if blank
|
||||||
|
if smtp_client.run_command("SUBJ"):
|
||||||
|
current_state_initialised = False
|
||||||
|
else:
|
||||||
|
# If a false was returned then they did not enter at least 1 address
|
||||||
|
print("You must have at least 1 recipient")
|
||||||
|
|
||||||
|
# Subject entery state, its not an official state since there is no real "SUBJ" command
|
||||||
|
elif smtp_client.get_state() == "SUBJ":
|
||||||
|
if not current_state_initialised:
|
||||||
|
print_title("Composing an Email: Subject")
|
||||||
|
print("Input \">QUIT\" at any point to quit back to the menu.")
|
||||||
|
print("Enter a Subject for this Email")
|
||||||
|
current_state_initialised = True
|
||||||
|
|
||||||
|
current_subject = input("SUBJECT: ")
|
||||||
|
# Quit the compose email loop and send the RSET command
|
||||||
|
if current_subject.upper() == ">QUIT":
|
||||||
|
current_state_initialised = False
|
||||||
|
smtp_client.run_command("RSET")
|
||||||
|
else:
|
||||||
|
if len(current_subject) == 0:
|
||||||
|
current_subject = "No Subject"
|
||||||
|
current_state_initialised = False
|
||||||
|
smtp_client.run_command("DATA", {"subject": current_subject})
|
||||||
|
|
||||||
|
# Enter the actual email body data entry mode, this will not end until you >QUIT or >SEND
|
||||||
|
elif smtp_client.get_state() == "DATA":
|
||||||
|
if not current_state_initialised:
|
||||||
|
print_title("Composing an Email: Body")
|
||||||
|
print("Input \">QUIT\" at any point to quit back to the menu.")
|
||||||
|
print("Input your email text, press enter to start a new line,")
|
||||||
|
print("Input \">SEND\" when you have finished your Email.")
|
||||||
|
current_state_initialised = True
|
||||||
|
|
||||||
|
current_mail_line = input("DATA: ")
|
||||||
|
# Quit the compose email loop and send the RSET command
|
||||||
|
if current_mail_line.upper() == ">QUIT":
|
||||||
|
current_state_initialised = False
|
||||||
|
smtp_client.run_command("RSET")
|
||||||
|
# Sends the \n.\n line to let the server know our data entry has ended
|
||||||
|
elif current_mail_line.upper() == ">SEND":
|
||||||
|
current_state_initialised = False
|
||||||
|
smtp_client.run_command("SEND", {'data_line': "\n.\n"})
|
||||||
|
else:
|
||||||
|
# Each line is sent individually when enter is pressed and stored on the server
|
||||||
|
smtp_client.run_command("LINE", {'data_line': current_mail_line})
|
||||||
|
|
||||||
|
# Output the mailbox or actual mail response from the server when in the VIEW state
|
||||||
|
elif smtp_client.get_state() == "VIEW":
|
||||||
|
print_title("Viewing Mail")
|
||||||
|
view_content = smtp_client.get_current_response_content()
|
||||||
|
for view_line in view_content:
|
||||||
|
print(view_line)
|
||||||
|
print("End of request, Enter a mail id, >LIST to view latest emails"
|
||||||
|
" or >QUIT to return to the menu.")
|
||||||
|
|
||||||
|
view_command = input("VIEW ")
|
||||||
|
smtp_client.run_command("VIEW", {"mail_id": view_command})
|
||||||
|
|
||||||
|
# Output the help response when the client is in the HELP state
|
||||||
|
elif smtp_client.get_state() == "HELP":
|
||||||
|
print_title("SMTP Server Help")
|
||||||
|
help_content = smtp_client.get_current_response_content()
|
||||||
|
for help_line in help_content:
|
||||||
|
print(help_line)
|
||||||
|
print("End of file, Enter a help subsection or >QUIT to return to the menu.")
|
||||||
|
|
||||||
|
help_command = input("HELP ")
|
||||||
|
smtp_client.run_command("HELP", {"help_section": help_command})
|
||||||
|
|
||||||
|
# QUIT should have already quit but if not we just pass
|
||||||
|
elif smtp_client.get_state() == "QUIT":
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Catch all for any commands, if we end here we've done something wrong
|
||||||
|
# Kept here to help debug if we do actually run into an issue
|
||||||
|
else:
|
||||||
|
current_command = input(f"{smtp_client.get_state()}: ")
|
||||||
|
smtp_client.run_command(current_command)
|
||||||
|
|
||||||
|
# If the client loses its active flag then the connection is no longer connected
|
||||||
|
print("Connection no longer active, exiting")
|
||||||
|
quit_flag = True
|
||||||
|
else:
|
||||||
|
if not do_a_quit("The connection could not be made, would you like to try another host?"):
|
||||||
|
# reset the host and port values to the defaults
|
||||||
|
host = default_host
|
||||||
|
port = default_port
|
||||||
|
give_up_flag = ""
|
||||||
|
else:
|
||||||
|
# quit the program loop
|
||||||
|
quit_flag = True
|
||||||
|
|
||||||
|
# the host given was invalid, ask if the user would like to try again
|
||||||
|
else:
|
||||||
|
if not do_a_quit("You did not enter a valid host, try again?"):
|
||||||
|
# reset the host and port values to the defaults
|
||||||
|
host = default_host
|
||||||
|
port = default_port
|
||||||
|
give_up_flag = ""
|
||||||
|
else:
|
||||||
|
# quit the program loop
|
||||||
|
quit_flag = True
|
||||||
|
|
||||||
|
# exit the program and say goodbye
|
||||||
|
print("Program ending, thank you")
|
284
Server/Crypto.py
Normal file
284
Server/Crypto.py
Normal file
@ -0,0 +1,284 @@
|
|||||||
|
# System Imports
|
||||||
|
import math
|
||||||
|
import random
|
||||||
|
|
||||||
|
# Project Imports
|
||||||
|
import SHA256Custom
|
||||||
|
|
||||||
|
|
||||||
|
class Receiver:
|
||||||
|
"""Class for dealing with the receiving end of the custom simple RSA encryption"""
|
||||||
|
def __init__(self):
|
||||||
|
# A list of small prime numbers we can use to calculate a larger one
|
||||||
|
self.__small_primes_list = [211, 223, 227, 229, 233, 239, 241, 251, 257, 263, 269, 271, 277, 281, 283, 293,
|
||||||
|
307, 311, 313, 317, 331, 337, 347, 349]
|
||||||
|
|
||||||
|
self.__p = None
|
||||||
|
self.__q = None
|
||||||
|
self.__n = None
|
||||||
|
self.__t = None
|
||||||
|
|
||||||
|
# This is apparently the most commonly used shared co-prime used for RSA
|
||||||
|
self.__e = 65537
|
||||||
|
self.__d = None
|
||||||
|
|
||||||
|
# The generated public and private keys
|
||||||
|
self.__public_key = None
|
||||||
|
self.__private_key = None
|
||||||
|
self.generated = False
|
||||||
|
|
||||||
|
# The bit size of the key
|
||||||
|
self.__bit_size = 1024
|
||||||
|
|
||||||
|
def generate_keys(self) -> bool:
|
||||||
|
"""Method to generate the RSA Public and Private keys for encryption"""
|
||||||
|
try:
|
||||||
|
# Only generate keys if we haven't already
|
||||||
|
if not self.generated:
|
||||||
|
self.__p = self.__generate_prime_number()
|
||||||
|
self.__q = self.__generate_prime_number()
|
||||||
|
|
||||||
|
# Make sure we have 2 different prime numbers
|
||||||
|
while self.__q == self.__p:
|
||||||
|
self.__q = self.__generate_prime_number()
|
||||||
|
|
||||||
|
# Most of the following is just the pseudo code for RSA prime generation
|
||||||
|
self.__n = self.__p * self.__q
|
||||||
|
self.__t = (self.__p - 1) * (self.__q - 1)
|
||||||
|
|
||||||
|
while math.gcd(self.__t, self.__e) > 1:
|
||||||
|
self.__e += 2
|
||||||
|
|
||||||
|
if self.__e < 1 or self.__e > self.__t:
|
||||||
|
raise Exception("E must be > 1 and < T")
|
||||||
|
|
||||||
|
if self.__t % self.__e == 0:
|
||||||
|
raise Exception("E is not a co-prime with T")
|
||||||
|
|
||||||
|
self.__d = Receiver.find_inverse(self.__e, self.__t)
|
||||||
|
|
||||||
|
# We have our keys, set up the tuples and flag the generated as true
|
||||||
|
self.__public_key = (self.__e, self.__n)
|
||||||
|
self.__private_key = (self.__d, self.__n)
|
||||||
|
self.generated = True
|
||||||
|
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
raise Exception("Keys have already been generated.")
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_expected_block_size(self) -> int:
|
||||||
|
"""Method to return the expected block size for the encrypted data"""
|
||||||
|
# the encrypted request blocksize seems to be half of the bit size
|
||||||
|
return int(self.__bit_size / 2)
|
||||||
|
|
||||||
|
def get_public_key_pair(self, hex_result=True):
|
||||||
|
"""Method for returning the public key as a hex string or tuple depending on the hex_results"""
|
||||||
|
if hex_result:
|
||||||
|
# Return both public key elements as a hex string
|
||||||
|
return "{0:x} {1:x}".format(self.__public_key[0], self.__public_key[1])
|
||||||
|
else:
|
||||||
|
return self.__public_key
|
||||||
|
|
||||||
|
def get_private_key_pair(self, hex_result=True):
|
||||||
|
"""Method for returning the private key as a hex string or tuple, not really used"""
|
||||||
|
if hex_result:
|
||||||
|
# Return both private key elements as a hex string
|
||||||
|
return "{0:x} {1:x}".format(self.__private_key[0], self.__private_key[1])
|
||||||
|
else:
|
||||||
|
return self.__private_key
|
||||||
|
|
||||||
|
def decrypt_string(self, string_in: str) -> str:
|
||||||
|
"""Method for decrypting our custom RSA encrypted blocks, takes the hex string in"""
|
||||||
|
if self.generated:
|
||||||
|
# Convert the hex string back into an int
|
||||||
|
encrypted_string_as_int = int(string_in, 16)
|
||||||
|
# Do the math on our encrypted int
|
||||||
|
decrypted_string_as_int = pow(encrypted_string_as_int, self.__d, self.__n)
|
||||||
|
# Convert the result back into a hex string
|
||||||
|
decrypted_string_as_hex = "{0:x}".format(decrypted_string_as_int)
|
||||||
|
|
||||||
|
start_padding_included = False
|
||||||
|
plain_text_read = False
|
||||||
|
|
||||||
|
plain_text_string = ''
|
||||||
|
hashed_checksum = ''
|
||||||
|
|
||||||
|
# Loop for every 2 characters (hex)
|
||||||
|
for i in range(0, len(decrypted_string_as_hex), 2):
|
||||||
|
current_byte = decrypted_string_as_hex[i:i+2]
|
||||||
|
# the encrypted string should start with a padding FF value
|
||||||
|
if not start_padding_included and current_byte.upper() == "FF":
|
||||||
|
start_padding_included = True
|
||||||
|
# the Checksum hash should start after the 00 value
|
||||||
|
elif not plain_text_read and start_padding_included and current_byte == "00":
|
||||||
|
plain_text_read = True
|
||||||
|
# Everything else should be the message or the checksum hash
|
||||||
|
elif start_padding_included:
|
||||||
|
if not plain_text_read:
|
||||||
|
# The message is padded after the fact to fill up the bit size, these are ignored
|
||||||
|
if current_byte.upper() != "FF":
|
||||||
|
# Convert the hex value back into a utf character
|
||||||
|
plain_text_string += chr(int(current_byte, 16))
|
||||||
|
else:
|
||||||
|
# Reconstruct the hash checksum
|
||||||
|
hashed_checksum += current_byte
|
||||||
|
|
||||||
|
# If the text of hash is missing then something went wrong with encrypting or sending
|
||||||
|
if len(plain_text_string) == 0 or len(hashed_checksum) == 0:
|
||||||
|
raise Exception("No text was decrypted, something must be wrong...")
|
||||||
|
|
||||||
|
# Hash the plain text string and make sure it matches our checksum hash
|
||||||
|
hash_test = SHA256Custom.SHA256Custom()
|
||||||
|
hash_test.update(plain_text_string.encode())
|
||||||
|
hashed_plain_text = hash_test.hexdigest()
|
||||||
|
|
||||||
|
if hashed_plain_text == hashed_checksum:
|
||||||
|
# The message has not been altered, decrypted successfully
|
||||||
|
return plain_text_string
|
||||||
|
else:
|
||||||
|
raise Exception("Plain text did not match the Hashed Checksum, something is wrong.")
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise Exception("No key has been generated")
|
||||||
|
|
||||||
|
def __generate_prime_number(self) -> int:
|
||||||
|
"""Private method for generating prime numbers with the bit size set"""
|
||||||
|
while True:
|
||||||
|
current_prime_candidate = self.__generate_low_level_prime(self.__bit_size)
|
||||||
|
if not self.miller_rabin_prime_check(current_prime_candidate):
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
return current_prime_candidate
|
||||||
|
|
||||||
|
def __generate_low_level_prime(self, prime_candidate: int) -> int:
|
||||||
|
"""Private method for generating the prime number using the candidate number and the small prime array"""
|
||||||
|
while True:
|
||||||
|
current_prime_candidate = self.generate_random_large_int(prime_candidate)
|
||||||
|
|
||||||
|
# We try to generate a large prime number using the candidate and looping through the small primes
|
||||||
|
# array until we get one
|
||||||
|
for divisor in self.__small_primes_list:
|
||||||
|
if current_prime_candidate % divisor == 0 and divisor ** 2 <= current_prime_candidate:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
return current_prime_candidate
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def generate_random_large_int(n_bit_size: int) -> int:
|
||||||
|
"""Static method to generate a large int value"""
|
||||||
|
# Get a random number in a range of the bit size given
|
||||||
|
return random.randrange(2 ** (n_bit_size - 1) + 1, 2 ** n_bit_size - 1)
|
||||||
|
|
||||||
|
# Method to test that the prime number we generate passes the miller rabin prime candidate test
|
||||||
|
@staticmethod
|
||||||
|
def miller_rabin_prime_check(prime_candidate: int) -> bool:
|
||||||
|
"""Static method for the miller rabin prime number checking algorithm, to ensure that we have a valid prime"""
|
||||||
|
max_divisions_by_two = 0
|
||||||
|
prime_minus_one = prime_candidate - 1
|
||||||
|
while prime_minus_one % 2 == 0:
|
||||||
|
prime_minus_one >>= 1
|
||||||
|
max_divisions_by_two += 1
|
||||||
|
|
||||||
|
assert 2 ** max_divisions_by_two * prime_minus_one == prime_candidate - 1
|
||||||
|
|
||||||
|
def trial_composite(round_tester_in):
|
||||||
|
"""Sub method for doing the actual prime number factorisation check"""
|
||||||
|
if pow(round_tester_in, prime_minus_one, prime_candidate) == 1:
|
||||||
|
return False
|
||||||
|
|
||||||
|
for x in range(max_divisions_by_two):
|
||||||
|
if pow(round_tester_in, 2 ** x * prime_minus_one, prime_candidate) == prime_candidate - 1:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
for i in range(0, 20):
|
||||||
|
round_tester = random.randrange(2, prime_candidate)
|
||||||
|
if trial_composite(round_tester):
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def extended_euclidean_algorithm(a: int, b: int) -> tuple:
|
||||||
|
"""Static method for the EEA from the pseudo code"""
|
||||||
|
if b == 0:
|
||||||
|
return 1, 0
|
||||||
|
|
||||||
|
q = a // b
|
||||||
|
r = a % b
|
||||||
|
s, t = Receiver.extended_euclidean_algorithm(b, r)
|
||||||
|
return t, s-(q*t)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def find_inverse(a: int, b: int) -> int:
|
||||||
|
"""Static method for finding the inverse of the primes given"""
|
||||||
|
inverse = Receiver.extended_euclidean_algorithm(a, b)[0]
|
||||||
|
if inverse < 1:
|
||||||
|
inverse += b
|
||||||
|
|
||||||
|
return inverse
|
||||||
|
|
||||||
|
|
||||||
|
class Transmitter:
|
||||||
|
"""Class for dealing with the transmitting end of the custom simple RSA encryption"""
|
||||||
|
def __init__(self, public_key: str):
|
||||||
|
# We should be getting the string hex values from the receiver and splitting them into the actual parts
|
||||||
|
public_key_parts = public_key.split(" ", 1)
|
||||||
|
|
||||||
|
# Must match the Receiver bit size
|
||||||
|
self.__bit_size = 1024
|
||||||
|
|
||||||
|
# Check if the public key has 2 parts to be valid
|
||||||
|
if len(public_key_parts) == 2:
|
||||||
|
self.__e = int(public_key_parts[0], 16)
|
||||||
|
self.__n = int(public_key_parts[1], 16)
|
||||||
|
self.__has_key = True
|
||||||
|
else:
|
||||||
|
self.__has_key = False
|
||||||
|
raise Exception("Public key was incorrect")
|
||||||
|
|
||||||
|
def get_current_key_pair(self):
|
||||||
|
"""Method for getting the key pair values e and n"""
|
||||||
|
return self.__e, self.__n
|
||||||
|
|
||||||
|
def get_usable_byte_length(self) -> int:
|
||||||
|
"""Method for getting the usable byte length incase we need to split a request into multiple parts"""
|
||||||
|
# Im not entirely sure on this as it was calculated mostly by "brute force" but it seems to be the
|
||||||
|
# key size divided by 4
|
||||||
|
# minus 2 bytes for the start and hash break bytes added
|
||||||
|
# minus 32 for the SHA256 hash size
|
||||||
|
# minus 32 for the RSA byte overhead, i've tried different values but getting too close to 256 bytes will
|
||||||
|
# sometimes case issues which i think is down to
|
||||||
|
return int(self.__bit_size / 4) - 2 - 32 - 32
|
||||||
|
|
||||||
|
def encrypt_string(self, string_in: bytes) -> bytes:
|
||||||
|
"""Method for encrypting a string using the public RSA key provided by the receiver"""
|
||||||
|
if self.__has_key:
|
||||||
|
# Create a hash of the message given for our checksum
|
||||||
|
string_hashing = SHA256Custom.SHA256Custom()
|
||||||
|
string_hashing.update(string_in)
|
||||||
|
|
||||||
|
string_hashed = string_hashing.hexdigest()
|
||||||
|
|
||||||
|
# Pad to make the string the full size of the custom RSA block
|
||||||
|
padding = ""
|
||||||
|
if len(string_in) < self.get_usable_byte_length():
|
||||||
|
padding_required = self.get_usable_byte_length() - len(string_in) - 1
|
||||||
|
padding = "ff" * padding_required
|
||||||
|
|
||||||
|
# Add an FF pad at the start of the string, since if the input hex starts with a 0X value the 0 is dropped
|
||||||
|
# when it is converted to an int, im not entirely sure how they get around this
|
||||||
|
hex_string = "ff" + string_in.hex() + padding + "00" + string_hashed
|
||||||
|
string_as_int = int(hex_string, 16)
|
||||||
|
|
||||||
|
# Do the math to the int value
|
||||||
|
encrypted_string_as_int = pow(string_as_int, self.__e, self.__n)
|
||||||
|
# Convert the int value into a hex string
|
||||||
|
encrypted_string_hex = "{0:x}".format(encrypted_string_as_int)
|
||||||
|
|
||||||
|
return encrypted_string_hex.encode()
|
||||||
|
else:
|
||||||
|
raise Exception("No key has been generated")
|
188
Server/DBConn.py
Normal file
188
Server/DBConn.py
Normal file
@ -0,0 +1,188 @@
|
|||||||
|
import sqlite3
|
||||||
|
import time
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
# added pairing type, mostly to stop it complaining about line length, only works on 3.9
|
||||||
|
# ParamPair = list[dict[str, any]]
|
||||||
|
|
||||||
|
|
||||||
|
def get_unix_timestamp(seconds_offset=0) -> int:
|
||||||
|
"""
|
||||||
|
Function to get the current unix timestamp to sim the NOW() sql method, seconds offset will pull a
|
||||||
|
past of future timestamp
|
||||||
|
"""
|
||||||
|
return int(time.time()) + seconds_offset
|
||||||
|
|
||||||
|
|
||||||
|
class DBConn:
|
||||||
|
"""Class to hold our methods for dealing with the SQLite3 Database"""
|
||||||
|
# start the database, which we would usually have a config file with the host, port, username etc
|
||||||
|
# but since sqlite just uses a file i've hard coded it
|
||||||
|
def __init__(self):
|
||||||
|
# Apparently outside of the pycharm env we need to tell it what DIR the DB is in, it should be in the server
|
||||||
|
# folder
|
||||||
|
path = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
db = os.path.join(path, 'server.db')
|
||||||
|
|
||||||
|
self.__db_conn = sqlite3.connect(db)
|
||||||
|
# set the row factory, to simulate the fetch_assoc rows other DB's use
|
||||||
|
self.__db_conn.row_factory = sqlite3.Row
|
||||||
|
self.__db_cursor = self.__db_conn.cursor()
|
||||||
|
|
||||||
|
def do_generic(self, query: str, return_values=False) -> any:
|
||||||
|
"""method for doing a generic SQL query, optionally return the result"""
|
||||||
|
result = False
|
||||||
|
if len(query) > 0:
|
||||||
|
try:
|
||||||
|
current_query = self.__db_cursor.execute(query)
|
||||||
|
if return_values:
|
||||||
|
result = current_query.fetchall()
|
||||||
|
else:
|
||||||
|
result = True
|
||||||
|
except Exception as ex:
|
||||||
|
print(f"Generic query Error: {repr(ex)}")
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def do_insert(self, table: str, insert_pairs, return_id=False):
|
||||||
|
"""Method for doing a generic insert query given the value pairs and table, we can return the insert id too"""
|
||||||
|
result = False
|
||||||
|
# Check if the table and insert values are not empty
|
||||||
|
if len(table) > 0 and len(insert_pairs) > 0:
|
||||||
|
fields = []
|
||||||
|
params = []
|
||||||
|
parsed_values = {}
|
||||||
|
# Using prepared statements so we dont get SQL injected
|
||||||
|
for pair in insert_pairs:
|
||||||
|
fields.append(pair["field"])
|
||||||
|
params.append(':' + pair["field"])
|
||||||
|
parsed_values[pair["field"]] = pair["value"]
|
||||||
|
|
||||||
|
# Construct the insert query with the input values
|
||||||
|
query_fields = ','.join(fields)
|
||||||
|
query_values = ','.join(params)
|
||||||
|
insert_query = "INSERT INTO {} ({}) VALUES ({})".format(table, query_fields, query_values)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if self.__db_cursor.execute(insert_query, parsed_values):
|
||||||
|
self.__db_conn.commit()
|
||||||
|
if return_id:
|
||||||
|
# Return the last row id if requested, otherwise return True
|
||||||
|
result = self.__db_cursor.lastrowid
|
||||||
|
else:
|
||||||
|
result = True
|
||||||
|
except Exception as ex:
|
||||||
|
print(f"Insert query Error: {repr(ex)}")
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def do_update(self, table: str, update_pairs, where: str, params) -> bool:
|
||||||
|
"""Method for doing a generic update query"""
|
||||||
|
result = False
|
||||||
|
if len(table) > 0 and len(update_pairs) > 0:
|
||||||
|
field_params = []
|
||||||
|
parsed_values = {}
|
||||||
|
# Setup the Update field/value pairs
|
||||||
|
for pair in update_pairs:
|
||||||
|
field_params.append("{} = {}".format(pair["field"], ':' + pair["field"]))
|
||||||
|
parsed_values[pair["field"]] = pair["value"]
|
||||||
|
|
||||||
|
# Add the values to the parameters
|
||||||
|
for param in params:
|
||||||
|
parsed_values[param["field"]] = param["value"]
|
||||||
|
|
||||||
|
# Construct the update query
|
||||||
|
query_fields = ','.join(field_params)
|
||||||
|
insert_query = "UPDATE {} SET {} WHERE {}".format(table, query_fields, where)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if self.__db_cursor.execute(insert_query, parsed_values):
|
||||||
|
self.__db_conn.commit()
|
||||||
|
result = True
|
||||||
|
except Exception as ex:
|
||||||
|
print(f"Update query Error: {repr(ex)}")
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def do_select(self, table: str, fetch_type="all", fields="", search="", params=None, query_extra=""):
|
||||||
|
"""Method for doing a generic select query"""
|
||||||
|
result = None
|
||||||
|
if len(table) > 0:
|
||||||
|
query_fields = "*"
|
||||||
|
query_block = ""
|
||||||
|
if len(fields) > 0:
|
||||||
|
# Check if the fields variable is a string or a list array
|
||||||
|
if isinstance(fields, str):
|
||||||
|
query_fields = fields
|
||||||
|
if isinstance(fields, list):
|
||||||
|
query_fields = ','.join(fields)
|
||||||
|
|
||||||
|
# Check if we have actual search parameters
|
||||||
|
if len(search) > 0:
|
||||||
|
query_block = " WHERE " + search
|
||||||
|
|
||||||
|
select_query = "SELECT {} FROM {}{} {}".format(query_fields, table, query_block, query_extra)
|
||||||
|
|
||||||
|
parsed_params = {}
|
||||||
|
for param in params:
|
||||||
|
parsed_params[param["field"]] = param["value"]
|
||||||
|
|
||||||
|
try:
|
||||||
|
db_query = self.__db_conn.execute(select_query, parsed_params)
|
||||||
|
# Return All, the Row or Column depending on the fetchtype passed in
|
||||||
|
if fetch_type == "all":
|
||||||
|
result = db_query.fetchall()
|
||||||
|
elif fetch_type == "row":
|
||||||
|
result = db_query.fetchone()
|
||||||
|
elif fetch_type == "col":
|
||||||
|
result = db_query.fetchone()[0]
|
||||||
|
except Exception as ex:
|
||||||
|
print(f"Select query Error: {repr(ex)}")
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def get_system_setting(self, key: str):
|
||||||
|
"""Method for getting a global system setting"""
|
||||||
|
setting_value = self.do_select("system_settings", "col", "setting_value", "setting_name = :setting_name",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"field": "setting_name",
|
||||||
|
"value": key
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
return setting_value
|
||||||
|
|
||||||
|
def set_system_setting(self, key: str, value) -> bool:
|
||||||
|
"""Method for setting a system setting"""
|
||||||
|
result = False
|
||||||
|
# Check if the setting exists, i would normally just use a INSERT ON DUPLICATE KEY UPDATE query, but it doesnt
|
||||||
|
# look like SQLite supports it
|
||||||
|
value_exists = self.do_select("system_settings", "col", "COUNT(*)", "setting_name = :setting_name",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"field": "setting_name",
|
||||||
|
"value": key
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
# If the key exists then update other wise insert it
|
||||||
|
if value_exists and value_exists > 0:
|
||||||
|
if self.do_update("system_settings",
|
||||||
|
[{"field": "setting_value", "value": value}],
|
||||||
|
"setting_name = :setting_name",
|
||||||
|
[{"field": "setting_name", "value": key}]):
|
||||||
|
result = True
|
||||||
|
else:
|
||||||
|
if self.do_insert("system_settings",
|
||||||
|
[{"field": "setting_name", "value": key},
|
||||||
|
{"field": "setting_value", "value": value}]):
|
||||||
|
result = True
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
"""Close the connection and cursor when the object is removed"""
|
||||||
|
self.__db_cursor.close()
|
||||||
|
self.__db_conn.close()
|
120
Server/Mail.py
Normal file
120
Server/Mail.py
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
import RawMail
|
||||||
|
import User
|
||||||
|
from DBConn import DBConn, get_unix_timestamp
|
||||||
|
|
||||||
|
|
||||||
|
class Mail:
|
||||||
|
"""Class for holding the data in the mail table of the database"""
|
||||||
|
def __init__(self):
|
||||||
|
self.__mail_id = None
|
||||||
|
self.__raw_id = None
|
||||||
|
self.__from_id = None
|
||||||
|
self.__to_id = None
|
||||||
|
self.__mail_subject = None
|
||||||
|
self.__mail_body = None
|
||||||
|
self.__mail_read = None
|
||||||
|
self.__mail_date_sent = None
|
||||||
|
|
||||||
|
# Holding variables for the from, to and raw mail class objects
|
||||||
|
self.__from_user: User.User or None = None
|
||||||
|
self.__to_user: User.User or None = None
|
||||||
|
self.__raw_mail: RawMail.RawMail or None = None
|
||||||
|
|
||||||
|
self.__is_valid = False
|
||||||
|
|
||||||
|
def load_mail(self, mail_id: int):
|
||||||
|
"""Method to load the mail with the id given"""
|
||||||
|
self.__mail_id = mail_id
|
||||||
|
self.__load_mail()
|
||||||
|
|
||||||
|
def __load_mail(self) -> None:
|
||||||
|
"""Private method to load the mail from the database using the mail_id"""
|
||||||
|
db_connection = DBConn()
|
||||||
|
mail_details = db_connection.do_select("mail_store", "row", "*", "mail_id = :mail_id",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"field": "mail_id",
|
||||||
|
"value": self.__mail_id
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
if mail_details is not None and len(mail_details) > 0:
|
||||||
|
self.__mail_id = mail_details['mail_id']
|
||||||
|
self.__raw_id = mail_details['raw_id']
|
||||||
|
self.__from_id = mail_details['from_id']
|
||||||
|
self.__to_id = mail_details['to_id']
|
||||||
|
self.__mail_subject = mail_details['mail_subject']
|
||||||
|
self.__mail_body = mail_details['mail_body']
|
||||||
|
self.__mail_read = mail_details['mail_read']
|
||||||
|
self.__mail_date_sent = mail_details['mail_date_sent']
|
||||||
|
self.__is_valid = True
|
||||||
|
|
||||||
|
def mail_is_valid(self):
|
||||||
|
"""Method to check if we have loaded a valid mail from the DB"""
|
||||||
|
return self.__is_valid
|
||||||
|
|
||||||
|
def get_mail_id(self):
|
||||||
|
"""Method to return the mail id"""
|
||||||
|
return self.__mail_id
|
||||||
|
|
||||||
|
def get_subject(self, trim_len: int = 0):
|
||||||
|
"""Method to return the subject line, if given a number it will truncate the string to that len"""
|
||||||
|
if trim_len > 0:
|
||||||
|
return self.__mail_subject[0:trim_len]
|
||||||
|
else:
|
||||||
|
return self.__mail_subject
|
||||||
|
|
||||||
|
def get_date_sent(self, format_string: str = "%d/%m/%y %H:%M"):
|
||||||
|
"""Method to get the sent date with the given date format"""
|
||||||
|
date_time_string = datetime.utcfromtimestamp(self.__mail_date_sent).strftime(format_string)
|
||||||
|
return date_time_string
|
||||||
|
|
||||||
|
def get_from_user(self) -> User.User or None:
|
||||||
|
"""Method to load a user object into the from user variable"""
|
||||||
|
if self.__from_id:
|
||||||
|
# Preload the user into the object if not loaded beforehand
|
||||||
|
if not self.__from_user:
|
||||||
|
self.__from_user = User.User(self.__from_id)
|
||||||
|
return self.__from_user
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_to_user(self) -> User.User or None:
|
||||||
|
"""Method to load a user object into the to user variable"""
|
||||||
|
if self.__to_id:
|
||||||
|
# Preload the user into the object if not loaded beforehand
|
||||||
|
if not self.__to_user:
|
||||||
|
self.__to_user = User.User(self.__to_id)
|
||||||
|
return self.__to_user
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_raw_mail(self):
|
||||||
|
"""Method to load a raw mail object into the variable"""
|
||||||
|
if not self.__raw_mail:
|
||||||
|
self.__raw_mail = RawMail.RawMail()
|
||||||
|
self.__raw_mail.load_raw_mail(self.__raw_id)
|
||||||
|
return self.__raw_mail
|
||||||
|
|
||||||
|
def create_mail(self, raw_id: int, from_id: int, to_id: int, subject: str, body: str) -> bool:
|
||||||
|
"""Method to add the mail to the database"""
|
||||||
|
result = False
|
||||||
|
if not self.__is_valid:
|
||||||
|
if raw_id and from_id and to_id and len(subject) > 0 or len(body) > 0:
|
||||||
|
db_connection = DBConn()
|
||||||
|
self.__mail_id = db_connection.do_insert("mail_store",
|
||||||
|
[{"field": "raw_id", "value": raw_id},
|
||||||
|
{"field": "from_id", "value": from_id},
|
||||||
|
{"field": "to_id", "value": to_id},
|
||||||
|
{"field": "mail_subject", "value": subject},
|
||||||
|
{"field": "mail_body", "value": body},
|
||||||
|
{"field": "mail_read", "value": 0},
|
||||||
|
{"field": "mail_date_sent", "value": get_unix_timestamp()}],
|
||||||
|
True)
|
||||||
|
if self.__mail_id:
|
||||||
|
self.__load_mail()
|
||||||
|
result = True
|
||||||
|
|
||||||
|
return result
|
135
Server/RawMail.py
Normal file
135
Server/RawMail.py
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
import User
|
||||||
|
from DBConn import DBConn, get_unix_timestamp
|
||||||
|
|
||||||
|
|
||||||
|
class RawMail:
|
||||||
|
"""Class for holding the raw mail variables whilst they are being added by a client"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.__raw_id = None
|
||||||
|
self.from_id = None
|
||||||
|
self.__raw_mail = None
|
||||||
|
self.__raw_date_sent = None
|
||||||
|
|
||||||
|
# Temp fields for holding the actual mail content, these can be parsed back from the raw_mail variable
|
||||||
|
self.from_address = None
|
||||||
|
self.to_address_list = []
|
||||||
|
self.subject = None
|
||||||
|
self.text_body = None
|
||||||
|
|
||||||
|
self.__from_user = None
|
||||||
|
|
||||||
|
self.__is_valid = False
|
||||||
|
|
||||||
|
def get_raw_id(self):
|
||||||
|
"""Method to return the raw id"""
|
||||||
|
return self.__raw_id
|
||||||
|
|
||||||
|
def get_raw_mail_content(self):
|
||||||
|
"""Method to get the raw mail body contents"""
|
||||||
|
return self.__raw_mail
|
||||||
|
|
||||||
|
def load_raw_mail(self, raw_id: int):
|
||||||
|
"""Method for loading a raw mail from its id"""
|
||||||
|
self.__raw_id = raw_id
|
||||||
|
self.__load_raw_mail()
|
||||||
|
|
||||||
|
def __load_raw_mail(self) -> None:
|
||||||
|
"""Private method to load the raw mail from the database"""
|
||||||
|
db_connection = DBConn()
|
||||||
|
raw_mail_details = db_connection.do_select("raw_mail", "row", "*", "raw_id = :raw_id",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"field": "raw_id",
|
||||||
|
"value": self.__raw_id
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
if raw_mail_details is not None and len(raw_mail_details) > 0:
|
||||||
|
self.__raw_id = raw_mail_details['raw_id']
|
||||||
|
self.from_id = raw_mail_details['from_id']
|
||||||
|
self.__raw_mail = raw_mail_details['raw_mail']
|
||||||
|
self.__raw_date_sent = raw_mail_details['raw_date_sent']
|
||||||
|
self.__is_valid = True
|
||||||
|
|
||||||
|
def raw_mail_is_valid(self):
|
||||||
|
"""Method to check if a valid raw mail has been loaded from the database"""
|
||||||
|
return self.__is_valid
|
||||||
|
|
||||||
|
def get_from_user(self) -> User.User or None:
|
||||||
|
"""Method to fill the from user variable with a user object"""
|
||||||
|
if self.from_id:
|
||||||
|
# Preload the user into the object if not loaded beforehand
|
||||||
|
if not self.__from_user:
|
||||||
|
self.__from_user = User.User(self.from_id)
|
||||||
|
return self.__from_user
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def strip_headers_from_body(self) -> str:
|
||||||
|
"""Method for stripping the header lines from the mail body for storing in the database mail_store"""
|
||||||
|
body_lines = self.text_body.split("\n")
|
||||||
|
body_text = ""
|
||||||
|
|
||||||
|
# There should be 4 "header" lines, but we dont just want to accept all lines that start with a header as
|
||||||
|
# the user could have entered one into the body of the email. Its not ideal but text parsers rarely are...
|
||||||
|
header_count = 0
|
||||||
|
for line in body_lines:
|
||||||
|
if (line.startswith("From: ") or line.startswith("Date: ") or line.startswith("To: ")) and header_count < 4:
|
||||||
|
header_count += 1
|
||||||
|
elif line.startswith("Subject: ") and header_count < 4:
|
||||||
|
header_count += 1
|
||||||
|
# Set the subject since that is only set in the data state with the body
|
||||||
|
if not self.subject:
|
||||||
|
self.subject = line.replace("Subject: ", "", 1)
|
||||||
|
else:
|
||||||
|
# Anything else should be the actual email content
|
||||||
|
body_text += line + "\n"
|
||||||
|
|
||||||
|
return body_text
|
||||||
|
|
||||||
|
def parse_raw_mail(self):
|
||||||
|
"""Method for parsing a raw mail header lines to extract the from, to, date and subject values"""
|
||||||
|
email_lines = self.__raw_mail.split("\n")
|
||||||
|
self.text_body = ""
|
||||||
|
|
||||||
|
# There should be 4 "header" lines, but we dont just want to accept all lines that start with a header as
|
||||||
|
# the user could have entered one into the body of the email. Its not ideal but text parsers rarely are...
|
||||||
|
header_count = 0
|
||||||
|
for line in email_lines:
|
||||||
|
if line.startswith("From: ") and header_count < 4:
|
||||||
|
# From user id is already stored in the actual raw email record
|
||||||
|
header_count += 1
|
||||||
|
pass
|
||||||
|
elif line.startswith("Date: ") and header_count < 4:
|
||||||
|
# The date is stored in the actual table so no need to text parse it
|
||||||
|
header_count += 1
|
||||||
|
pass
|
||||||
|
elif line.startswith("To: ") and header_count < 4:
|
||||||
|
header_count += 1
|
||||||
|
if len(self.to_address_list) == 0:
|
||||||
|
addresses_only = line.replace("To: ", "", 1)
|
||||||
|
self.to_address_list = addresses_only.split(",")
|
||||||
|
elif line.startswith("Subject: ") and header_count < 4:
|
||||||
|
header_count += 1
|
||||||
|
if not self.subject:
|
||||||
|
self.subject = line.replace("Subject: ", "", 1)
|
||||||
|
else:
|
||||||
|
self.text_body += line + "\n"
|
||||||
|
|
||||||
|
def create_raw_mail(self, from_id: int, raw: str) -> bool:
|
||||||
|
"""Method for saving the raw mail into the database"""
|
||||||
|
result = False
|
||||||
|
if not self.__is_valid:
|
||||||
|
if from_id and len(raw) > 0:
|
||||||
|
db_connection = DBConn()
|
||||||
|
self.__raw_id = db_connection.do_insert("raw_mail",
|
||||||
|
[{"field": "from_id", "value": from_id},
|
||||||
|
{"field": "raw_mail", "value": raw},
|
||||||
|
{"field": "raw_date_sent", "value": get_unix_timestamp()}],
|
||||||
|
True)
|
||||||
|
if self.__raw_id:
|
||||||
|
self.__load_raw_mail()
|
||||||
|
result = True
|
||||||
|
|
||||||
|
return result
|
126
Server/SHA256Custom.py
Normal file
126
Server/SHA256Custom.py
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
class SHA256Custom:
|
||||||
|
"""
|
||||||
|
Custom SHA256 function, its not really custom but rewritten using the pseudocode from wikipedia and
|
||||||
|
should mimic the built in function
|
||||||
|
"""
|
||||||
|
def __init__(self, data=None):
|
||||||
|
# Setting up the initial bit values, i wont pretend to understand the math involved but these seem hardcoded
|
||||||
|
self.__ks = [0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
|
||||||
|
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
|
||||||
|
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
||||||
|
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
|
||||||
|
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
|
||||||
|
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
||||||
|
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
|
||||||
|
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2]
|
||||||
|
|
||||||
|
self.__hs = [0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19]
|
||||||
|
|
||||||
|
# Copy the initial bit values for use since they are changed
|
||||||
|
self.__h = self.__hs[:]
|
||||||
|
self.__k = self.__ks[:]
|
||||||
|
|
||||||
|
# Create variables we will need in the class
|
||||||
|
self.__string_byte_len = 0
|
||||||
|
self.__buffer = b''
|
||||||
|
self.__string_hashed = False
|
||||||
|
self.__digested_string = b''
|
||||||
|
|
||||||
|
if data is not None:
|
||||||
|
self.update(data)
|
||||||
|
|
||||||
|
# Static methods because pycharm is moaning about them not needing to be inside of the class
|
||||||
|
@staticmethod
|
||||||
|
def pad_string(string_len: int) -> bytes:
|
||||||
|
"""Static method to pad the string digest for the hash"""
|
||||||
|
# Adding a "1" bit to the end, although it seems to use 63 as the actual value, im guessing its possibly to do
|
||||||
|
# with chunk size
|
||||||
|
string_di = string_len & 0x3f
|
||||||
|
# Get the 64bit big endian length of the post processed hash length
|
||||||
|
post_process_length = (string_len << 3).to_bytes(8, "big")
|
||||||
|
padded_length = 55
|
||||||
|
if string_di < 56:
|
||||||
|
padded_length -= string_di
|
||||||
|
else:
|
||||||
|
padded_length -= 199 - string_di
|
||||||
|
|
||||||
|
return b'\x80' + b'\x00' * padded_length + post_process_length
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def rotate_right(a, b):
|
||||||
|
"""Static method to rotate a and b bits right """
|
||||||
|
return ((a >> b) | (a << (32 - b))) & 0xffffffff
|
||||||
|
|
||||||
|
def update(self, string_in: bytes) -> None:
|
||||||
|
"""Method to update the hash value"""
|
||||||
|
if (string_in and len(string_in) > 0) and not self.__string_hashed:
|
||||||
|
self.__string_byte_len += len(string_in)
|
||||||
|
string_buffer = self.__buffer + string_in
|
||||||
|
|
||||||
|
# Process the string as 512 bit chunks (64 bytes)
|
||||||
|
for i in range(0, len(string_buffer) // 64):
|
||||||
|
self.calculate_hash(string_buffer[64 * i:64 * (i + 1)])
|
||||||
|
|
||||||
|
# Set the class buffer
|
||||||
|
self.__buffer = string_buffer[len(string_buffer) - (len(string_buffer) % 64):]
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
|
||||||
|
def digest(self) -> bytes:
|
||||||
|
"""Method to get the bytes digest of our hash value"""
|
||||||
|
if not self.__string_hashed:
|
||||||
|
self.update(self.pad_string(self.__string_byte_len))
|
||||||
|
self.__digested_string = b''.join(val.to_bytes(4, 'big') for val in self.__h[:8])
|
||||||
|
self.__string_hashed = True
|
||||||
|
return self.__digested_string
|
||||||
|
|
||||||
|
def hexdigest(self) -> str:
|
||||||
|
"""Method to get the hex digest of our hash value"""
|
||||||
|
# Hex characters to use when changing the hex value back into a string
|
||||||
|
tab = '0123456789abcdef'
|
||||||
|
return "".join(tab[byt >> 4] + tab[byt & 0xf] for byt in self.digest())
|
||||||
|
|
||||||
|
def calculate_hash(self, chunk) -> None:
|
||||||
|
"""Method to calculate the actual hash value"""
|
||||||
|
# Create an array of 32bit words
|
||||||
|
words = [0] * 64
|
||||||
|
|
||||||
|
# Converted from the pseudo code, but i believe its filling the word array with first 16 words we add
|
||||||
|
words[0:16] = [int.from_bytes(chunk[i:i + 4], "big") for i in range(0, len(chunk), 4)]
|
||||||
|
|
||||||
|
for i in range(16, 64):
|
||||||
|
s0 = self.rotate_right(words[i - 15], 7) ^ self.rotate_right(words[i - 15], 18) ^ (words[i - 15] >> 3)
|
||||||
|
s1 = self.rotate_right(words[i - 2], 17) ^ self.rotate_right(words[i - 2], 19) ^ (words[i - 2] >> 10)
|
||||||
|
words[i] = (words[i - 16] + s0 + words[i - 7] + s1) & 0xffffffff
|
||||||
|
|
||||||
|
# Initialize the values with the default hash value
|
||||||
|
a, b, c, d, e, f, g, h = self.__h
|
||||||
|
|
||||||
|
# Calculation main loop
|
||||||
|
for i in range(64):
|
||||||
|
s0 = self.rotate_right(a, 2) ^ self.rotate_right(a, 13) ^ self.rotate_right(a, 22)
|
||||||
|
t2 = s0 + self.maj(a, b, c)
|
||||||
|
s1 = self.rotate_right(e, 6) ^ self.rotate_right(e, 11) ^ self.rotate_right(e, 25)
|
||||||
|
t1 = h + s1 + self.ch(e, f, g) + self.__k[i] + words[i]
|
||||||
|
|
||||||
|
h = g
|
||||||
|
g = f
|
||||||
|
f = e
|
||||||
|
e = (d + t1) & 0xffffffff
|
||||||
|
d = c
|
||||||
|
c = b
|
||||||
|
b = a
|
||||||
|
a = (t1 + t2) & 0xffffffff
|
||||||
|
|
||||||
|
for i, (x, y) in enumerate(zip(self.__h, [a, b, c, d, e, f, g, h])):
|
||||||
|
self.__h[i] = (x + y) & 0xffffffff
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def maj(a, b, c):
|
||||||
|
"""Static method for the calculation of the maj variable in the pseudo code"""
|
||||||
|
return (a & b) ^ (a & c) ^ (b & c)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def ch(a, b, c):
|
||||||
|
"""Static method to calculate the ch variable in the pseudo code"""
|
||||||
|
return (a & b) ^ ((~a) & c)
|
71
Server/SMTPServer.py
Normal file
71
Server/SMTPServer.py
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import selectors
|
||||||
|
import socket
|
||||||
|
import ServerThread
|
||||||
|
|
||||||
|
|
||||||
|
class SMTPServer:
|
||||||
|
def __init__(self, host: str, port: int):
|
||||||
|
self.__host = host
|
||||||
|
self.__port = port
|
||||||
|
self.__socket = None
|
||||||
|
self.__selector = selectors.DefaultSelector()
|
||||||
|
|
||||||
|
self.__server_threads = []
|
||||||
|
self.active = False
|
||||||
|
|
||||||
|
def start_server(self) -> bool:
|
||||||
|
server_address = (self.__host, self.__port)
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.__socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
self.__socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
|
self.__socket.bind(server_address)
|
||||||
|
self.__socket.listen()
|
||||||
|
self.__socket.setblocking(False)
|
||||||
|
self.__selector.register(self.__socket, selectors.EVENT_READ, data=None)
|
||||||
|
self.active = True
|
||||||
|
|
||||||
|
print(f"Server Listening on {self.__host}:{self.__port}")
|
||||||
|
result = True
|
||||||
|
except OSError as ex:
|
||||||
|
self.__socket.close()
|
||||||
|
self.__socket = None
|
||||||
|
print(f"Error opening the listener: {repr(ex)}")
|
||||||
|
result = False
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def accept_wrapper(self, socket_in: socket):
|
||||||
|
connection, address = socket_in.accept()
|
||||||
|
print("Received Connection from", address)
|
||||||
|
|
||||||
|
connection.setblocking(False)
|
||||||
|
new_server_thread = ServerThread.ServerThread(connection, address)
|
||||||
|
self.__server_threads.append(new_server_thread)
|
||||||
|
new_server_thread.start()
|
||||||
|
|
||||||
|
def open_server(self):
|
||||||
|
try:
|
||||||
|
while self.active:
|
||||||
|
events = self.__selector.select(timeout=None)
|
||||||
|
|
||||||
|
for key, mask in events:
|
||||||
|
if key.data is None:
|
||||||
|
self.accept_wrapper(key.fileobj)
|
||||||
|
else:
|
||||||
|
pass
|
||||||
|
except KeyboardInterrupt as ex:
|
||||||
|
print("Keyboard Exception")
|
||||||
|
for child_thread in self.__server_threads:
|
||||||
|
child_thread.active = False
|
||||||
|
self.active = False
|
||||||
|
finally:
|
||||||
|
self.__selector.close()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
126
Server/ServerLog.py
Normal file
126
Server/ServerLog.py
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
import Session
|
||||||
|
import User
|
||||||
|
from DBConn import DBConn, get_unix_timestamp
|
||||||
|
|
||||||
|
|
||||||
|
class ServerLog:
|
||||||
|
"""Class for holding a server log entry from the database"""
|
||||||
|
def __init__(self, log_id: int = None):
|
||||||
|
self.__log_id = log_id
|
||||||
|
self.__session_ref = None
|
||||||
|
self.__log_action_dir = None
|
||||||
|
self.__log_action = None
|
||||||
|
self.__log_client_ip = None
|
||||||
|
self.__log_date = None
|
||||||
|
self.__is_valid = False
|
||||||
|
|
||||||
|
self.__session: Session.Session or None = None
|
||||||
|
self.__user: User.User or None = None
|
||||||
|
|
||||||
|
# if a log id is provided load the record from the database
|
||||||
|
if self.__log_id:
|
||||||
|
self.__load_server_log()
|
||||||
|
|
||||||
|
def __load_server_log(self) -> None:
|
||||||
|
"""Private method for loading a server log from the database with its id"""
|
||||||
|
db_connection = DBConn()
|
||||||
|
server_log_details = db_connection.do_select("server_logs", "row", "*", "log_id = :log_id",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"field": "log_id",
|
||||||
|
"value": self.__log_id
|
||||||
|
}
|
||||||
|
])
|
||||||
|
if server_log_details is not None and len(server_log_details) > 0:
|
||||||
|
self.__log_id = server_log_details['log_id']
|
||||||
|
self.__session_ref = server_log_details['session_ref']
|
||||||
|
self.__log_action_dir = server_log_details['log_action_dir']
|
||||||
|
self.__log_action = server_log_details['log_action']
|
||||||
|
self.__log_client_ip = server_log_details['log_client_ip']
|
||||||
|
self.__log_date = server_log_details['log_date']
|
||||||
|
self.__is_valid = True
|
||||||
|
|
||||||
|
def server_log_is_valid(self) -> bool:
|
||||||
|
"""Method to check if a valid server log has been loaded from the database"""
|
||||||
|
return self.__is_valid
|
||||||
|
|
||||||
|
def get_log_action_dir(self) -> str:
|
||||||
|
"""Method to get the logs direction variable"""
|
||||||
|
return self.__log_action_dir
|
||||||
|
|
||||||
|
def get_log_action(self) -> str:
|
||||||
|
"""Method to get the log action variable"""
|
||||||
|
return self.__log_action
|
||||||
|
|
||||||
|
def get_log_action_with_dir(self) -> str:
|
||||||
|
"""Method to return both the log direction and action as a string"""
|
||||||
|
return f"{self.get_log_action_dir()} {self.get_log_action()}"
|
||||||
|
|
||||||
|
def output_log_line(self, output_format_string: str, date_format_string: str = "%d/%m/%y %H:%M") -> str:
|
||||||
|
"""Method for writing the log as a string in the format given"""
|
||||||
|
date_time_string = datetime.utcfromtimestamp(self.__log_date).strftime(date_format_string)
|
||||||
|
|
||||||
|
# Check if the log has a user and get the user name, otherwise it was an out of session action
|
||||||
|
if self.get_user():
|
||||||
|
username = self.get_user().username
|
||||||
|
else:
|
||||||
|
username = "N/A"
|
||||||
|
|
||||||
|
log_string = output_format_string.format(
|
||||||
|
date_time_string,
|
||||||
|
username,
|
||||||
|
self.__log_client_ip,
|
||||||
|
self.__log_action_dir,
|
||||||
|
self.__log_action[0: 40]
|
||||||
|
)
|
||||||
|
|
||||||
|
return log_string
|
||||||
|
|
||||||
|
def get_session(self) -> Session.Session or None:
|
||||||
|
"""Method for getting the session object to use in the session variable"""
|
||||||
|
if self.__session_ref and self.__session_ref != "":
|
||||||
|
if not self.__session:
|
||||||
|
self.__session = Session.Session(self.__session_ref, None)
|
||||||
|
self.__session.load_session()
|
||||||
|
return self.__session
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_user(self) -> User.User or None:
|
||||||
|
"""Method for getting the user object for the user variable"""
|
||||||
|
if self.__session_ref and self.__session_ref != "":
|
||||||
|
if self.get_session():
|
||||||
|
# Preload the user into the object if not loaded beforehand
|
||||||
|
if not self.__user:
|
||||||
|
self.__user = self.__session.get_user()
|
||||||
|
return self.__user
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def create_server_log(self, session_ref: str, direction: str, action: str or bytes, ip: str) -> bool:
|
||||||
|
"""Method for saving the current server log action to the database"""
|
||||||
|
result = False
|
||||||
|
if not self.__is_valid:
|
||||||
|
if len(direction) > 0 and len(action) > 0 and len(ip) > 0:
|
||||||
|
db_connection = DBConn()
|
||||||
|
|
||||||
|
# Force any byte strings into normal strings before putting it into the DB
|
||||||
|
try:
|
||||||
|
action = action.decode()
|
||||||
|
except (UnicodeDecodeError, AttributeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
if db_connection.do_insert("server_logs",
|
||||||
|
[{"field": "session_ref", "value": session_ref},
|
||||||
|
{"field": "log_action_dir", "value": direction},
|
||||||
|
{"field": "log_action", "value": action},
|
||||||
|
{"field": "log_client_ip", "value": ip},
|
||||||
|
{"field": "log_date", "value": get_unix_timestamp()}]):
|
||||||
|
self.__load_server_log()
|
||||||
|
result = True
|
||||||
|
|
||||||
|
return result
|
742
Server/ServerThread.py
Normal file
742
Server/ServerThread.py
Normal file
@ -0,0 +1,742 @@
|
|||||||
|
# System imports
|
||||||
|
import random
|
||||||
|
import string
|
||||||
|
import socket
|
||||||
|
import selectors
|
||||||
|
import queue
|
||||||
|
import uuid
|
||||||
|
from threading import Thread
|
||||||
|
from time import sleep
|
||||||
|
|
||||||
|
# My class imports
|
||||||
|
import Crypto
|
||||||
|
import Mail
|
||||||
|
import RawMail
|
||||||
|
import ServerLog
|
||||||
|
import Session
|
||||||
|
import User
|
||||||
|
from DBConn import DBConn, get_unix_timestamp
|
||||||
|
|
||||||
|
|
||||||
|
class ServerThread(Thread):
|
||||||
|
"""Class for holding the server thread specific methods"""
|
||||||
|
def __init__(self, current_socket: socket, address: tuple):
|
||||||
|
Thread.__init__(self)
|
||||||
|
|
||||||
|
self.__address = address
|
||||||
|
self.__socket = current_socket
|
||||||
|
self.__selector = selectors.DefaultSelector()
|
||||||
|
|
||||||
|
self.__buffer_in = queue.Queue()
|
||||||
|
self.__buffer_out = queue.Queue()
|
||||||
|
|
||||||
|
# flag for the current network status, -1 for awaiting request, 0 for awaiting response, 1 for ready
|
||||||
|
self.current_status = -1
|
||||||
|
self.active = True
|
||||||
|
self.initialised = False
|
||||||
|
self.__pending_close = False
|
||||||
|
self.__main_state = "INIT"
|
||||||
|
self.__sub_state = ""
|
||||||
|
|
||||||
|
# Holding objects for the current session and raw mail
|
||||||
|
self.__current_session: Session.Session or None = None
|
||||||
|
self.__current_mail: RawMail.RawMail or None = None
|
||||||
|
|
||||||
|
# Queue array for queued responses
|
||||||
|
self.__queued_messages = []
|
||||||
|
|
||||||
|
# Variables needed for the custom RSA encryption
|
||||||
|
self.__pending_handshake = False
|
||||||
|
self.__hand_shook = False
|
||||||
|
self.__crypt_test_phrase = None
|
||||||
|
self.__crypto_manager_receive: Crypto.Receiver or None = None
|
||||||
|
self.__crypto_manager_transmit: Crypto.Transmitter or None = None
|
||||||
|
|
||||||
|
self.__selector.register(self.__socket, selectors.EVENT_READ | selectors.EVENT_WRITE, data=None)
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
"""Method for doing the threads main loop"""
|
||||||
|
pending_close_counter = 0
|
||||||
|
pending_handshake_counter = 0
|
||||||
|
timed_out = False
|
||||||
|
try:
|
||||||
|
while self.active:
|
||||||
|
waiting_commands = self.__selector.select(timeout=None)
|
||||||
|
for key, mask in waiting_commands:
|
||||||
|
try:
|
||||||
|
if mask & selectors.EVENT_READ:
|
||||||
|
self.__read()
|
||||||
|
elif mask & selectors.EVENT_WRITE and not self.__buffer_out.empty():
|
||||||
|
# Write/Send the initial message
|
||||||
|
self.__write()
|
||||||
|
elif mask & selectors.EVENT_WRITE and len(self.__queued_messages) > 0:
|
||||||
|
# Write/Send any other queued messages for multipart responses
|
||||||
|
self.__write_queue()
|
||||||
|
except Exception as ex:
|
||||||
|
print(ex)
|
||||||
|
self.__close()
|
||||||
|
|
||||||
|
# If the connection has not been initialised send the initial service ready response
|
||||||
|
if not self.initialised:
|
||||||
|
self.__process_smtp_response(220, self.__address[0] + " SMTP service ready")
|
||||||
|
self.__main_state = "HELO"
|
||||||
|
self.initialised = True
|
||||||
|
|
||||||
|
# If the user is logged in but the connection has not received a heartbeat or request in the time then
|
||||||
|
# close te connection and send the timeout response
|
||||||
|
if self.__current_session and not timed_out:
|
||||||
|
if self.__current_session.get_session_last_action_timestamp() < get_unix_timestamp(-300):
|
||||||
|
self.__process_smtp_response(421, "Timeout, closing connection")
|
||||||
|
timed_out = True
|
||||||
|
|
||||||
|
# Check for a pending handshake, we dont want to flag it straight away as it will start encoding the
|
||||||
|
# response before we are ready, this will allow the current request to be processed before flipping it
|
||||||
|
if self.__pending_handshake:
|
||||||
|
if pending_handshake_counter == 1:
|
||||||
|
self.__pending_handshake = False
|
||||||
|
self.__hand_shook = True
|
||||||
|
pending_handshake_counter = 0
|
||||||
|
else:
|
||||||
|
pending_handshake_counter += 1
|
||||||
|
|
||||||
|
# Check if we have a pending close request, this allows us to send the good bye response before rudely
|
||||||
|
# ending the connection
|
||||||
|
if self.__pending_close:
|
||||||
|
if pending_close_counter == 1:
|
||||||
|
self.active = False
|
||||||
|
self.__close()
|
||||||
|
else:
|
||||||
|
pending_close_counter += 1
|
||||||
|
|
||||||
|
if not self.__selector.get_map():
|
||||||
|
break
|
||||||
|
|
||||||
|
finally:
|
||||||
|
self.__selector.close()
|
||||||
|
|
||||||
|
def __read(self):
|
||||||
|
"""Private method for reading the response from the client"""
|
||||||
|
try:
|
||||||
|
received = self.__socket.recv(4096)
|
||||||
|
except BlockingIOError as ex:
|
||||||
|
print(f"IO Error: {repr(ex)}")
|
||||||
|
else:
|
||||||
|
if received:
|
||||||
|
# If we have received a response, and the hand is shook and its not a plain emergency quit response we
|
||||||
|
# decrypt the data here
|
||||||
|
if self.__hand_shook and received.decode() not in ["QUIT"]:
|
||||||
|
received_decoded_string = received.decode()
|
||||||
|
# The request is split into the chunks needed and decrypted
|
||||||
|
chunk_size = self.__crypto_manager_receive.get_expected_block_size()
|
||||||
|
received_chunks = [self.__crypto_manager_receive.decrypt_string(
|
||||||
|
received_decoded_string[i:i + chunk_size]
|
||||||
|
) for i in range(0, len(received_decoded_string), chunk_size)]
|
||||||
|
# Once we have decrypted all of the request blocks we can join them back into 1 string
|
||||||
|
full_received = "".join(received_chunks)
|
||||||
|
print(f"Received request: {repr(full_received)} from Client: {self.__address}")
|
||||||
|
self.__buffer_in.put(full_received)
|
||||||
|
|
||||||
|
# We log the previous action in the session object and the time of the last action
|
||||||
|
if self.__current_session:
|
||||||
|
state_string = self.__main_state
|
||||||
|
if self.__sub_state:
|
||||||
|
state_string += "." + self.__sub_state
|
||||||
|
self.__current_session.update_session(state_string, full_received)
|
||||||
|
|
||||||
|
# and log the action in the server logs
|
||||||
|
self.__write_log_entry("IN", full_received)
|
||||||
|
else:
|
||||||
|
# If we are not hand shook then we treat the request as normal and log the
|
||||||
|
# action in the server logs
|
||||||
|
self.__buffer_in.put(received.decode())
|
||||||
|
print(f"Received request: {repr(received)} from Client: {self.__address}")
|
||||||
|
self.__write_log_entry("IN", received)
|
||||||
|
else:
|
||||||
|
raise RuntimeError("Connection Read Error, peer disconnected?")
|
||||||
|
|
||||||
|
self.__process_smtp_request()
|
||||||
|
|
||||||
|
def __write_queue(self):
|
||||||
|
"""Private method for processing the write queue"""
|
||||||
|
# Some SMTP response require the server to send multiple responses per request
|
||||||
|
while len(self.__queued_messages) > 0:
|
||||||
|
# Sleep to simulate some kind of latency to stop the queue to hopefully stop
|
||||||
|
# the response from being added to the previous.
|
||||||
|
sleep(1)
|
||||||
|
# Pop each item from the array until we have nothing
|
||||||
|
item = self.__queued_messages.pop(0)
|
||||||
|
self.__process_smtp_response(item['code'], item['message'])
|
||||||
|
self.__write()
|
||||||
|
|
||||||
|
self.__queued_messages.clear()
|
||||||
|
|
||||||
|
def __write(self):
|
||||||
|
"""Private method for doing a normal socket write"""
|
||||||
|
try:
|
||||||
|
response = self.__buffer_out.get_nowait()
|
||||||
|
except Exception:
|
||||||
|
response = None
|
||||||
|
|
||||||
|
if response:
|
||||||
|
print(f"Sending Response: {response} to Client: {self.__address}")
|
||||||
|
try:
|
||||||
|
self.__do_write(response)
|
||||||
|
except BlockingIOError as ex:
|
||||||
|
print(f"IO Error: {repr(ex)}")
|
||||||
|
pass
|
||||||
|
|
||||||
|
def __do_write(self, response: bytes) -> None:
|
||||||
|
"""Private method shared between the write methods to do the actual socket write and encryption"""
|
||||||
|
# If the hand is shook then do the encryption
|
||||||
|
if self.__hand_shook:
|
||||||
|
# Write the log item before encrypting so we know what it was
|
||||||
|
self.__write_log_entry("OUT", response)
|
||||||
|
# Split into the chunk sizes we need/if needed
|
||||||
|
chunk_size = self.__crypto_manager_transmit.get_usable_byte_length()
|
||||||
|
response_chunks = [self.__crypto_manager_transmit.encrypt_string(response[i:i + chunk_size])
|
||||||
|
for i in range(0, len(response), chunk_size)]
|
||||||
|
# Join the request back together and send
|
||||||
|
full_response = b"".join(response_chunks)
|
||||||
|
self.__socket.send(full_response)
|
||||||
|
else:
|
||||||
|
# Otherwise just send as normal and log the server action
|
||||||
|
self.__socket.send(response)
|
||||||
|
self.__write_log_entry("OUT", response)
|
||||||
|
|
||||||
|
def __write_log_entry(self, direction: str, action: str or bytes) -> None:
|
||||||
|
"""Private method to write a log to the server logs table"""
|
||||||
|
# Check if the client is logged in and has an active session to log
|
||||||
|
if self.__current_session:
|
||||||
|
session_ref = self.__current_session.session_unique_ref
|
||||||
|
else:
|
||||||
|
session_ref = ""
|
||||||
|
|
||||||
|
# Check if its the login request and remove the password since logging that would be pretty silly...
|
||||||
|
try:
|
||||||
|
if bytes(action).startswith(b"LOGI"):
|
||||||
|
action_parts = action.split(" ")
|
||||||
|
if len(action_parts) == 3:
|
||||||
|
action = "LOGI " + action_parts[1]
|
||||||
|
else:
|
||||||
|
action = "LOGI Empty/Bad Request"
|
||||||
|
# The auth requests are too long and storing the keys seems dumb so ignore those too
|
||||||
|
elif bytes(action).startswith(b"AUTH"):
|
||||||
|
action = "AUTH Auth sending"
|
||||||
|
elif bytes(action).startswith(b"570"):
|
||||||
|
action = "570 Auth sending"
|
||||||
|
# Multipart requests also flood the table as well as self replicate when checking, ignore those as we log
|
||||||
|
# the initial request anyway
|
||||||
|
elif bytes(action).startswith(b"214"):
|
||||||
|
return
|
||||||
|
|
||||||
|
ServerLog.ServerLog().create_server_log(session_ref, direction, action, self.__address[0])
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def __queue_smtp_response(self, code: int, message: str):
|
||||||
|
"""Private method for queuing an SMTP response to the client"""
|
||||||
|
self.__queued_messages.append({"code": code, "message": message})
|
||||||
|
|
||||||
|
def __process_smtp_response(self, code: int, message: str):
|
||||||
|
"""Private method for sending an SMTP response to a client """
|
||||||
|
data_out = str(code)
|
||||||
|
if len(message) > 0:
|
||||||
|
data_out += " " + message
|
||||||
|
|
||||||
|
encoded_data_out = data_out.encode()
|
||||||
|
self.__buffer_out.put(encoded_data_out)
|
||||||
|
|
||||||
|
def __process_smtp_request(self):
|
||||||
|
"""Private method for processing a request from the client and sending responses back"""
|
||||||
|
current_command = self.__buffer_in.get()
|
||||||
|
|
||||||
|
# Split the request as it should be in a format like XXXX YYYYYYYYYYYY, where Y would be any optional data
|
||||||
|
current_command_parts = current_command.split(" ", 1)
|
||||||
|
|
||||||
|
# Create a connection to the DB
|
||||||
|
db_connection = DBConn()
|
||||||
|
|
||||||
|
# Check if we are in Data entry mode, otherwise process the command as normal
|
||||||
|
if current_command_parts[0] not in ["RSET", "NOOP"] \
|
||||||
|
and (self.__main_state == "MAIL" and self.__sub_state == "DATA"):
|
||||||
|
# Check if we receive the end data input line
|
||||||
|
if current_command != "\n.\n":
|
||||||
|
self.__current_mail.text_body += current_command + "\n"
|
||||||
|
else:
|
||||||
|
# If it has been we process the mail and "send" it to the users
|
||||||
|
self.__current_mail.create_raw_mail(self.__current_session.get_user().get_user_id(),
|
||||||
|
self.__current_mail.text_body)
|
||||||
|
email_body = self.__current_mail.strip_headers_from_body()
|
||||||
|
|
||||||
|
for recipient in self.__current_mail.to_address_list:
|
||||||
|
# The local recipients are added as their user id when processing the RCPT requests
|
||||||
|
if isinstance(recipient, int):
|
||||||
|
temp_email = Mail.Mail()
|
||||||
|
temp_email.create_mail(self.__current_mail.get_raw_id(),
|
||||||
|
self.__current_mail.from_id,
|
||||||
|
recipient,
|
||||||
|
self.__current_mail.subject,
|
||||||
|
email_body)
|
||||||
|
else:
|
||||||
|
# We dont really care about the non local recipients,
|
||||||
|
# but they would be forwarded to their server
|
||||||
|
pass
|
||||||
|
# Mail has been saved, send the OK and set us back to the main menu state
|
||||||
|
self.__process_smtp_response(250, "Ok")
|
||||||
|
self.__main_state = "HMEN"
|
||||||
|
self.__sub_state = None
|
||||||
|
|
||||||
|
else:
|
||||||
|
if len(current_command_parts) > 0:
|
||||||
|
# Check if the user is logged in when required
|
||||||
|
if current_command_parts[0] not in ["HELO", "NOOP", "AUTH", "HSHK", "LOGI"]:
|
||||||
|
if self.__current_session is not None:
|
||||||
|
# User is not valid, boot them from the server
|
||||||
|
if self.__current_session.get_user() and not self.__current_session.get_user().user_logged_in():
|
||||||
|
self.__process_smtp_response(421, "Session invalid, Goodbye")
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
# Session is invalid boot the client
|
||||||
|
self.__process_smtp_response(421, "Session invalid, Goodbye")
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Respond to the HELO command
|
||||||
|
if current_command_parts[0] == "HELO":
|
||||||
|
if self.__main_state == "HELO":
|
||||||
|
self.__process_smtp_response(250, "Greetings from " + self.__address[0])
|
||||||
|
# Setup the custom RSA encryption variables and generate the keys to send
|
||||||
|
self.__crypto_manager_receive = Crypto.Receiver()
|
||||||
|
self.__crypto_manager_receive.generate_keys()
|
||||||
|
# Queue the public key response to send after the previous response
|
||||||
|
self.__queue_smtp_response(570, self.__crypto_manager_receive.get_public_key_pair())
|
||||||
|
self.__main_state = "AUTH"
|
||||||
|
else:
|
||||||
|
# We arnt expecting a HELO command, most likely because we didnt initialise
|
||||||
|
self.__process_smtp_response(503, "Bad sequence of commands")
|
||||||
|
|
||||||
|
# Response to the AUTH state requests
|
||||||
|
elif current_command_parts[0] == "AUTH":
|
||||||
|
if self.__main_state == "AUTH":
|
||||||
|
if len(current_command_parts) == 2:
|
||||||
|
# We should have received the public key from the client, we generate a random string
|
||||||
|
# to send during the handshake to make sure both public keys are working correctly
|
||||||
|
self.__crypto_manager_transmit = Crypto.Transmitter(current_command_parts[1])
|
||||||
|
self.__crypt_test_phrase = "".join(
|
||||||
|
random.choices(string.ascii_uppercase + string.digits, k=10))
|
||||||
|
self.__queue_smtp_response(250, self.__crypt_test_phrase)
|
||||||
|
self.__main_state = "HSHK"
|
||||||
|
self.__hand_shook = True
|
||||||
|
else:
|
||||||
|
# If we didnt get a valid request the handshake most likely errored, we may as well
|
||||||
|
# close the connection
|
||||||
|
print(f"Bad handshake from {self.__address}")
|
||||||
|
self.__process_smtp_response(421, "Bad handshake, bye")
|
||||||
|
self.__pending_close = True
|
||||||
|
|
||||||
|
elif current_command_parts[0] == "HSHK":
|
||||||
|
if self.__main_state == "HSHK":
|
||||||
|
if len(current_command_parts) == 2:
|
||||||
|
# Check if the client returned the correct test phrase back, if so the encryption
|
||||||
|
# is working both ways and we should be safe to proceed
|
||||||
|
if current_command_parts[1] == self.__crypt_test_phrase:
|
||||||
|
self.__queue_smtp_response(250, "OK")
|
||||||
|
self.__main_state = "LOGI"
|
||||||
|
else:
|
||||||
|
# If the phrase is wrong there is obviously an issue with the encryption keys and
|
||||||
|
# we just close the connection so the user can try again
|
||||||
|
print(f"Bad handshake response from {self.__address}")
|
||||||
|
self.__process_smtp_response(421, "Bad handshake, bye")
|
||||||
|
self.__pending_close = True
|
||||||
|
else:
|
||||||
|
# If the response didnt have the right amount of elements then the handshake failed and we
|
||||||
|
# close the connection so the user can try again
|
||||||
|
print(f"Bad handshake response from {self.__address}")
|
||||||
|
self.__process_smtp_response(421, "Bad handshake, bye")
|
||||||
|
self.__pending_close = True
|
||||||
|
|
||||||
|
# Process the NOOP Keep alive/Heartbeat response request
|
||||||
|
elif current_command_parts[0] == "NOOP":
|
||||||
|
self.__process_smtp_response(295, "OK")
|
||||||
|
|
||||||
|
# Process the LOGI login request
|
||||||
|
elif current_command_parts[0] == "LOGI":
|
||||||
|
# If the client is already logged in or the server is not expecting the login command
|
||||||
|
# send the bad sequence response
|
||||||
|
if self.__main_state != "LOGI" \
|
||||||
|
or (self.__current_session and self.__current_session.session_is_valid()):
|
||||||
|
# if the user is already logged in then they should not be sending the LOGI command
|
||||||
|
self.__process_smtp_response(503, "Bad sequence of commands")
|
||||||
|
|
||||||
|
elif len(current_command_parts) == 2 and len(current_command_parts[1]) > 0:
|
||||||
|
# Login parts 0 should be the username and 1 the password
|
||||||
|
login_parts = current_command_parts[1].split(" ", 1)
|
||||||
|
# We should have 2 login parts
|
||||||
|
if len(login_parts) == 2:
|
||||||
|
user_login = User.User(login_parts[0])
|
||||||
|
current_login = user_login.login(login_parts[1], self.__address[0])
|
||||||
|
|
||||||
|
# The user has successfully logged in
|
||||||
|
if current_login['status_code'] == 1:
|
||||||
|
# Generate a UUID to use as the sessions unique ID
|
||||||
|
session_uuid = str(uuid.uuid4())
|
||||||
|
self.__current_session = Session.Session(session_uuid, user_login)
|
||||||
|
self.__current_session.create_session(
|
||||||
|
user_login.get_user_id(),
|
||||||
|
self.__address[0]
|
||||||
|
)
|
||||||
|
self.__process_smtp_response(250, "OK, Welcome " + user_login.username)
|
||||||
|
# Send the session reference back so the client can use this to verify itself
|
||||||
|
self.__queue_smtp_response(265, self.__current_session.session_unique_ref)
|
||||||
|
self.__main_state = "HMEN"
|
||||||
|
|
||||||
|
elif current_login['status_code'] == -1:
|
||||||
|
# Too many login attempts for this user so the connection is rejected
|
||||||
|
self.__process_smtp_response(421, "Authentication failed, your connection has been "
|
||||||
|
"terminated, please wait and try again later")
|
||||||
|
self.__pending_close = True
|
||||||
|
|
||||||
|
else:
|
||||||
|
# 535 is the only error code that relates to authentication
|
||||||
|
self.__process_smtp_response(535, "Authentication failed")
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.__process_smtp_response(535, "Authentication failed")
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Syntax error because they did not send the command correctly
|
||||||
|
self.__process_smtp_response(501, "Syntax Error")
|
||||||
|
|
||||||
|
# Respond to the VRFY request
|
||||||
|
elif current_command_parts[0] == "VRFY":
|
||||||
|
if len(current_command_parts) == 2:
|
||||||
|
# Get the logged in users name and email address
|
||||||
|
verify_user = User.User(current_command_parts[1])
|
||||||
|
if self.__current_session.get_user().get_user_id() == verify_user.get_user_id():
|
||||||
|
self.__queue_smtp_response(261, verify_user.role.upper())
|
||||||
|
self.__queue_smtp_response(250, verify_user.get_user_fullname()
|
||||||
|
+ " <" + verify_user.email_address + ">")
|
||||||
|
else:
|
||||||
|
self.__process_smtp_response(421, "Session invalid, Goodbye")
|
||||||
|
else:
|
||||||
|
self.__process_smtp_response(500, "Invalid command given")
|
||||||
|
|
||||||
|
# Respond to the Log out Command
|
||||||
|
elif current_command_parts[0] == "LOUT":
|
||||||
|
logged_in_user = self.__current_session.get_user()
|
||||||
|
if logged_in_user and logged_in_user.user_logged_in():
|
||||||
|
self.__current_session = None
|
||||||
|
self.__process_smtp_response(250, "OK, Goodbye " + logged_in_user.username)
|
||||||
|
self.__main_state = "LOGI"
|
||||||
|
self.__sub_state = None
|
||||||
|
else:
|
||||||
|
# the user wasn't logged in so we shouldn't be getting this request
|
||||||
|
self.__process_smtp_response(503, "Bad sequence of commands")
|
||||||
|
|
||||||
|
# Process the MAIL command
|
||||||
|
elif current_command_parts[0] == "MAIL":
|
||||||
|
if self.__main_state == "HMEN":
|
||||||
|
if len(current_command_parts) == 2:
|
||||||
|
# To start the mail process the client should be sending the FROM address to us
|
||||||
|
mail_command_parts = current_command_parts[1].split(" ", 1)
|
||||||
|
if len(mail_command_parts) == 2 and mail_command_parts[0] == "FROM" \
|
||||||
|
and len(mail_command_parts[1].strip(" <>")) > 0:
|
||||||
|
# Create a raw mail for us to add the temp fields in before creating the mail
|
||||||
|
self.__current_mail = RawMail.RawMail()
|
||||||
|
self.__current_mail.from_id = self.__current_session.get_user().get_user_id()
|
||||||
|
self.__current_mail.from_address = mail_command_parts[1].strip(" <>")
|
||||||
|
self.__main_state = "MAIL"
|
||||||
|
self.__sub_state = "RCPT"
|
||||||
|
self.__process_smtp_response(250, "OK")
|
||||||
|
else:
|
||||||
|
# Bad Email address sent
|
||||||
|
self.__process_smtp_response(500, "Invalid command given")
|
||||||
|
else:
|
||||||
|
# Bad command sent
|
||||||
|
self.__process_smtp_response(500, "Invalid command given")
|
||||||
|
else:
|
||||||
|
# Tried to mail from the wrong state
|
||||||
|
self.__process_smtp_response(503, "Bad sequence of commands")
|
||||||
|
|
||||||
|
# Process the RCPT request
|
||||||
|
elif current_command_parts[0] == "RCPT":
|
||||||
|
if self.__sub_state == "RCPT":
|
||||||
|
if len(current_command_parts) == 2:
|
||||||
|
# Split the command so that we can extract the email address
|
||||||
|
rcpt_command_parts = current_command_parts[1].split(" ", 1)
|
||||||
|
if len(rcpt_command_parts) == 2 and rcpt_command_parts[0] == "TO" \
|
||||||
|
and len(rcpt_command_parts[1].strip(" <>")) > 0:
|
||||||
|
email_cleaned = rcpt_command_parts[1].strip(" <>")
|
||||||
|
email_parts = email_cleaned.split("@", 1)
|
||||||
|
if len(email_parts) == 2:
|
||||||
|
# Check if the email address is from the local domain from the site settings
|
||||||
|
if email_parts[1] == db_connection.get_system_setting("SERVER_DOMAIN"):
|
||||||
|
local_user = db_connection.do_select("users", "col", "user_id",
|
||||||
|
"user_email = :user_email",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"field": "user_email",
|
||||||
|
"value": email_cleaned
|
||||||
|
}
|
||||||
|
])
|
||||||
|
if local_user and int(local_user) > 0:
|
||||||
|
# Address is a valid local address
|
||||||
|
if local_user not in self.__current_mail.to_address_list:
|
||||||
|
self.__current_mail.to_address_list.append(local_user)
|
||||||
|
self.__process_smtp_response(250, "Ok")
|
||||||
|
else:
|
||||||
|
# Address is already in the list...
|
||||||
|
self.__process_smtp_response(500, "Invalid command given")
|
||||||
|
else:
|
||||||
|
# Address does not exist locally
|
||||||
|
self.__process_smtp_response(550, "Mailbox unavailable")
|
||||||
|
else:
|
||||||
|
# The email uses a non local domain, so we pretend we can forward it
|
||||||
|
if email_cleaned not in self.__current_mail.to_address_list:
|
||||||
|
self.__current_mail.to_address_list.append(email_cleaned)
|
||||||
|
self.__process_smtp_response(251,
|
||||||
|
"User not local, will attempt to forward")
|
||||||
|
else:
|
||||||
|
# Address is already in the list...
|
||||||
|
self.__process_smtp_response(500, "Invalid command given")
|
||||||
|
else:
|
||||||
|
self.__process_smtp_response(500, "Not a valid email address")
|
||||||
|
else:
|
||||||
|
self.__process_smtp_response(500, "Invalid command given")
|
||||||
|
else:
|
||||||
|
self.__process_smtp_response(503, "Bad sequence of commands")
|
||||||
|
|
||||||
|
# Start the DATA request response
|
||||||
|
elif current_command_parts[0] == "DATA":
|
||||||
|
if self.__main_state == "MAIL" and self.__sub_state == "RCPT":
|
||||||
|
if len(self.__current_mail.to_address_list) > 0:
|
||||||
|
# Set the server into data entry mode, this will cause it to loop at the start
|
||||||
|
# of this method instead of match commands until the end characters are sent
|
||||||
|
self.__process_smtp_response(354, "Start mail input; end with <CRLF>.<CRLF>")
|
||||||
|
self.__sub_state = "DATA"
|
||||||
|
self.__current_mail.text_body = ""
|
||||||
|
else:
|
||||||
|
self.__process_smtp_response(500, "Invalid command given")
|
||||||
|
else:
|
||||||
|
self.__process_smtp_response(503, "Bad sequence of commands")
|
||||||
|
|
||||||
|
# Process the VIEW request
|
||||||
|
elif current_command_parts[0] == "VIEW":
|
||||||
|
if len(current_command_parts) == 2:
|
||||||
|
view_command_parts = current_command_parts[1].split(" ", 1)
|
||||||
|
# The client wants to quit the VIEW state
|
||||||
|
if view_command_parts[0] == "QUIT":
|
||||||
|
self.__process_smtp_response(250, "OK")
|
||||||
|
self.__main_state = "HMEN"
|
||||||
|
self.__sub_state = None
|
||||||
|
# The client wants to view a mail
|
||||||
|
elif view_command_parts[0] == "MAIL":
|
||||||
|
self.__main_state = "VIEW"
|
||||||
|
self.__sub_state = "MAIL"
|
||||||
|
|
||||||
|
user_id = self.__current_session.get_user().get_user_id()
|
||||||
|
mail_id = view_command_parts[1]
|
||||||
|
|
||||||
|
mail_item = db_connection.do_select("mail_store", "row", "mail_id",
|
||||||
|
"mail_id = :mail_id AND to_id = :to_id",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"field": "mail_id",
|
||||||
|
"value": mail_id
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"field": "to_id",
|
||||||
|
"value": user_id
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
# Return the mail if the query returns it, it must be one of their emails and a valid
|
||||||
|
# mail id
|
||||||
|
if mail_item:
|
||||||
|
current_mail = Mail.Mail()
|
||||||
|
current_mail.load_mail(mail_item['mail_id'])
|
||||||
|
|
||||||
|
mail_body = current_mail.get_raw_mail().get_raw_mail_content()
|
||||||
|
mail_body_lines = mail_body.split("\n")
|
||||||
|
|
||||||
|
# Send each line of the email then the output ending string
|
||||||
|
self.__process_smtp_response(211, "OK")
|
||||||
|
for line in mail_body_lines:
|
||||||
|
if len(line) > 0:
|
||||||
|
self.__queue_smtp_response(214, line)
|
||||||
|
self.__queue_smtp_response(214, "\n.\n")
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.__process_smtp_response(550, "Could not find the mail id")
|
||||||
|
|
||||||
|
# Client wants to view the mails in their mailbox
|
||||||
|
elif view_command_parts[0] == "LIST":
|
||||||
|
self.__main_state = "VIEW"
|
||||||
|
self.__sub_state = "LIST"
|
||||||
|
|
||||||
|
user_id = self.__current_session.get_user().get_user_id()
|
||||||
|
|
||||||
|
mail_box_content = ["Last 20 mailbox items:", ""]
|
||||||
|
mail_box_items = db_connection.do_select("mail_store", "all", "mail_id",
|
||||||
|
"to_id = :to_id",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"field": "to_id",
|
||||||
|
"value": user_id
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ORDER BY mail_date_sent DESC LIMIT 0, 20")
|
||||||
|
for row in mail_box_items:
|
||||||
|
current_mail = Mail.Mail()
|
||||||
|
current_mail.load_mail(row['mail_id'])
|
||||||
|
mail_box_content.append("{:<8} | {:<25} | "
|
||||||
|
"{:<25} | {:<15}".format(current_mail.get_mail_id(),
|
||||||
|
current_mail.get_subject(20),
|
||||||
|
current_mail.get_from_user()
|
||||||
|
.get_user_fullname(),
|
||||||
|
current_mail.get_date_sent()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(mail_box_content) == 2:
|
||||||
|
mail_box_content[1] = "No Items."
|
||||||
|
else:
|
||||||
|
mail_box_content[1] = "{:<8} | {:<25} | {:<25} | {:<15}".format("ID", "Subject",
|
||||||
|
"From", "Date")
|
||||||
|
|
||||||
|
self.__process_smtp_response(211, "OK")
|
||||||
|
for line in mail_box_content:
|
||||||
|
self.__queue_smtp_response(214, line)
|
||||||
|
self.__queue_smtp_response(214, "\n.\n")
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.__process_smtp_response(500, "Invalid command given")
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.__process_smtp_response(500, "Invalid command given")
|
||||||
|
|
||||||
|
# Process the RSET command and return to the home menu state
|
||||||
|
elif current_command_parts[0] == "RSET":
|
||||||
|
self.__current_mail = None
|
||||||
|
self.__process_smtp_response(250, "Ok")
|
||||||
|
self.__main_state = "HMEN"
|
||||||
|
self.__sub_state = None
|
||||||
|
|
||||||
|
# Process the HELP request
|
||||||
|
elif current_command_parts[0] == "HELP":
|
||||||
|
help_content = ""
|
||||||
|
# If no help "section" is requested get the MAIN help text
|
||||||
|
if len(current_command_parts) == 1:
|
||||||
|
help_content = db_connection.get_system_setting("HELPMAIN")
|
||||||
|
# Set the state to help mode
|
||||||
|
self.__main_state = "HELP"
|
||||||
|
elif len(current_command_parts) == 2 and self.__main_state == "HELP":
|
||||||
|
# The client is trying to quit the help state
|
||||||
|
if current_command_parts[1] == "QUIT":
|
||||||
|
self.__process_smtp_response(250, "OK")
|
||||||
|
self.__main_state = "HMEN"
|
||||||
|
self.__sub_state = None
|
||||||
|
else:
|
||||||
|
help_content = db_connection.get_system_setting("HELP" + current_command_parts[1])
|
||||||
|
else:
|
||||||
|
# If the client is asking for a HELP section and we arnt in the help state they're wrong
|
||||||
|
if self.__main_state != "HELP":
|
||||||
|
self.__process_smtp_response(503, "Bad sequence of commands")
|
||||||
|
else:
|
||||||
|
self.__process_smtp_response(500, "Invalid command given")
|
||||||
|
|
||||||
|
# If we have help content to actually send back the content
|
||||||
|
if help_content and len(help_content) > 0:
|
||||||
|
self.__process_smtp_response(211, "OK")
|
||||||
|
help_content_lines = help_content.split("\n")
|
||||||
|
for line in help_content_lines:
|
||||||
|
self.__queue_smtp_response(214, line)
|
||||||
|
self.__queue_smtp_response(214, "\n.\n")
|
||||||
|
else:
|
||||||
|
if self.__main_state == "HELP":
|
||||||
|
self.__process_smtp_response(504, "Command parameter is not implemented")
|
||||||
|
|
||||||
|
# ADMIN only commands
|
||||||
|
elif current_command_parts[0] == "ADMN":
|
||||||
|
# Check the logged in user is actually an admin
|
||||||
|
if self.__current_session.get_user().role.upper() == "ADMIN":
|
||||||
|
if len(current_command_parts) == 2:
|
||||||
|
if current_command_parts[1] == "HOME":
|
||||||
|
self.__main_state = "ADMN"
|
||||||
|
self.__process_smtp_response(250, "OK")
|
||||||
|
# The client is trying to quit the admin menu
|
||||||
|
elif current_command_parts[1] == "QUIT":
|
||||||
|
self.__main_state = "HMEN"
|
||||||
|
self.__sub_state = None
|
||||||
|
self.__process_smtp_response(250, "OK QUIT")
|
||||||
|
# Unlock a user that has been locked out
|
||||||
|
elif current_command_parts[1].startswith("UNLK"):
|
||||||
|
unlock_parts = current_command_parts[1].split(" ", 1)
|
||||||
|
if len(unlock_parts) == 2:
|
||||||
|
unlock_user = User.User(unlock_parts[1])
|
||||||
|
if unlock_user.valid_user():
|
||||||
|
unlock_user.unlock()
|
||||||
|
self.__process_smtp_response(250, "OK DONE")
|
||||||
|
else:
|
||||||
|
self.__process_smtp_response(500, "User not found.")
|
||||||
|
else:
|
||||||
|
self.__process_smtp_response(500, "Invalid command given")
|
||||||
|
# Command to return the last 20 items from the server logs audit
|
||||||
|
elif current_command_parts[1].startswith("AUDT"):
|
||||||
|
server_log_content = ["Last 20 server actions:", ""]
|
||||||
|
server_log_items = db_connection.do_select("server_logs", "all", "log_id", "", [],
|
||||||
|
"ORDER BY log_date DESC "
|
||||||
|
"LIMIT 0, 20")
|
||||||
|
for row in server_log_items:
|
||||||
|
current_log = ServerLog.ServerLog(row['log_id'])
|
||||||
|
server_log_content.append(current_log.output_log_line("{:<14} | {:<14} "
|
||||||
|
"| {:<15} | {:<3} "
|
||||||
|
"| {:<30} "))
|
||||||
|
|
||||||
|
if len(server_log_content) == 2:
|
||||||
|
server_log_content[1] = "No Items."
|
||||||
|
else:
|
||||||
|
server_log_content[1] = "{:<14} | " \
|
||||||
|
"{:<14} | " \
|
||||||
|
"{:<15} | " \
|
||||||
|
"{:<3} | " \
|
||||||
|
"{:<30} ".format("Date", "User", "IP", "Dir", "Action")
|
||||||
|
|
||||||
|
self.__process_smtp_response(211, "OK")
|
||||||
|
for line in server_log_content:
|
||||||
|
self.__queue_smtp_response(214, line)
|
||||||
|
self.__queue_smtp_response(214, "\n.\n")
|
||||||
|
else:
|
||||||
|
self.__process_smtp_response(500, "Invalid command given")
|
||||||
|
else:
|
||||||
|
self.__process_smtp_response(535, "No access")
|
||||||
|
|
||||||
|
# The client wants to quit, send the goodbye response
|
||||||
|
elif current_command_parts[0] == "QUIT":
|
||||||
|
self.__process_smtp_response(221, "Bye")
|
||||||
|
print(f"Client {repr(self.__address[0])} has quit...")
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.__process_smtp_response(500, "Invalid command given")
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
# Catch all for any errors thrown during this loop
|
||||||
|
self.__process_smtp_response(500, "Invalid command given")
|
||||||
|
# print(f"Error: {repr(ex)}")
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.__process_smtp_response(500, "Invalid command given")
|
||||||
|
|
||||||
|
def __close(self):
|
||||||
|
"""Private method to close the server thread"""
|
||||||
|
print("Attempting to end the Connection...")
|
||||||
|
try:
|
||||||
|
self.__selector.unregister(self.__socket)
|
||||||
|
self.__socket.close()
|
||||||
|
print(f"Connection ended with ", self.__address)
|
||||||
|
except OSError as ex:
|
||||||
|
print(f"Could not close the connection: {repr(ex)}")
|
||||||
|
finally:
|
||||||
|
self.__socket = None
|
||||||
|
self.active = False
|
||||||
|
self.current_status = -1
|
101
Server/Session.py
Normal file
101
Server/Session.py
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
import User
|
||||||
|
from DBConn import DBConn, get_unix_timestamp
|
||||||
|
|
||||||
|
|
||||||
|
class Session:
|
||||||
|
"""Class to hold a users login session methods and variables"""
|
||||||
|
def __init__(self, session_ref, user: User.User or None):
|
||||||
|
self.__session_id = None
|
||||||
|
self.session_unique_ref = session_ref
|
||||||
|
self.__user_id = None
|
||||||
|
self.__session_ip = None
|
||||||
|
self.__session_data = None
|
||||||
|
self.__session_state = None
|
||||||
|
self.__session_last_action_date = None
|
||||||
|
self.__is_valid = False
|
||||||
|
|
||||||
|
self.__user: User.User or None = user
|
||||||
|
|
||||||
|
def __load_session(self) -> None:
|
||||||
|
"""Private method to load the users session from the database"""
|
||||||
|
db_connection = DBConn()
|
||||||
|
session_details = db_connection.do_select("sessions", "row", "*", "session_unique_ref = :session_unique_ref",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"field": "session_unique_ref",
|
||||||
|
"value": self.session_unique_ref
|
||||||
|
}
|
||||||
|
])
|
||||||
|
if session_details is not None and len(session_details) > 0:
|
||||||
|
self.__session_id = session_details['session_id']
|
||||||
|
self.session_unique_ref = session_details['session_unique_ref']
|
||||||
|
self.__user_id = session_details['user_id']
|
||||||
|
self.__session_ip = session_details['session_ip']
|
||||||
|
self.__session_data = session_details['session_data']
|
||||||
|
self.__session_state = session_details['session_state']
|
||||||
|
self.__session_last_action_date = session_details['session_last_action_date']
|
||||||
|
self.__is_valid = True
|
||||||
|
|
||||||
|
def session_is_valid(self):
|
||||||
|
"""Method to check if the session has a valid record loaded"""
|
||||||
|
return self.__is_valid
|
||||||
|
|
||||||
|
def load_session(self) -> None:
|
||||||
|
"""Method to load a session if the unique reference has been set"""
|
||||||
|
if self.session_unique_ref:
|
||||||
|
self.__load_session()
|
||||||
|
|
||||||
|
def get_session_last_action_timestamp(self):
|
||||||
|
"""Method to get the sessions last action timestamp"""
|
||||||
|
return self.__session_last_action_date
|
||||||
|
|
||||||
|
def get_user(self) -> User.User or None:
|
||||||
|
"""Method to get the user object for the variable"""
|
||||||
|
if self.__user_id:
|
||||||
|
# Preload the user into the object if not loaded beforehand
|
||||||
|
if not self.__user:
|
||||||
|
self.__user = User.User(self.__user_id)
|
||||||
|
return self.__user
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def update_session(self, current_state: str, last_action: str) -> None:
|
||||||
|
"""Method to update the session with the last action, current state and current timestamp"""
|
||||||
|
self.__session_data = last_action
|
||||||
|
self.__session_state = current_state
|
||||||
|
self.__session_last_action_date = get_unix_timestamp()
|
||||||
|
self.save_current_changes()
|
||||||
|
|
||||||
|
def save_current_changes(self) -> bool:
|
||||||
|
"""Method to save the changes made to a session to the database"""
|
||||||
|
result = False
|
||||||
|
if self.__is_valid:
|
||||||
|
db_connection = DBConn()
|
||||||
|
if db_connection.do_update("sessions",
|
||||||
|
[{"field": "session_data", "value": self.__session_data},
|
||||||
|
{"field": "session_state", "value": self.__session_state},
|
||||||
|
{"field": "session_last_action_date",
|
||||||
|
"value": self.__session_last_action_date}],
|
||||||
|
"session_id = :session_id",
|
||||||
|
[{"field": "session_id", "value": self.__session_id}]):
|
||||||
|
result = True
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def create_session(self, user_id: int, ip: str, data="", state="") -> bool:
|
||||||
|
"""Method to save a new session to the database"""
|
||||||
|
result = False
|
||||||
|
if not self.__is_valid:
|
||||||
|
if user_id and len(ip) > 0:
|
||||||
|
db_connection = DBConn()
|
||||||
|
if db_connection.do_insert("sessions",
|
||||||
|
[{"field": "session_unique_ref", "value": self.session_unique_ref},
|
||||||
|
{"field": "user_id", "value": user_id},
|
||||||
|
{"field": "session_ip", "value": ip},
|
||||||
|
{"field": "session_data", "value": data},
|
||||||
|
{"field": "session_state", "value": state},
|
||||||
|
{"field": "session_last_action_date", "value": get_unix_timestamp()}]):
|
||||||
|
self.__load_session()
|
||||||
|
result = True
|
||||||
|
|
||||||
|
return result
|
208
Server/User.py
Normal file
208
Server/User.py
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
import random
|
||||||
|
import string
|
||||||
|
import time
|
||||||
|
|
||||||
|
import SHA256Custom
|
||||||
|
from DBConn import DBConn, get_unix_timestamp
|
||||||
|
|
||||||
|
|
||||||
|
class User:
|
||||||
|
"""Class for dealing with a User object from the database"""
|
||||||
|
def __init__(self, user_id):
|
||||||
|
# Check if the user id is being added as an int for the user_id or string for the username
|
||||||
|
if isinstance(user_id, int):
|
||||||
|
self.username = None
|
||||||
|
self.__user_id = user_id
|
||||||
|
elif isinstance(user_id, str):
|
||||||
|
self.username = user_id
|
||||||
|
self.__user_id = None
|
||||||
|
self.first_name = ""
|
||||||
|
self.last_name = ""
|
||||||
|
self.email_address = ""
|
||||||
|
self.login_ip = ""
|
||||||
|
self.role = ""
|
||||||
|
self.__is_valid = False
|
||||||
|
self.__is_logged_in = False
|
||||||
|
self.login_date = -1
|
||||||
|
self.__load_user()
|
||||||
|
|
||||||
|
def user_logged_in(self):
|
||||||
|
"""Method to check if the user is logged in"""
|
||||||
|
return self.__is_logged_in
|
||||||
|
|
||||||
|
def get_user_id(self):
|
||||||
|
"""Method to return the users id"""
|
||||||
|
return self.__user_id
|
||||||
|
|
||||||
|
def get_user_fullname(self):
|
||||||
|
"""Method to return the users full name"""
|
||||||
|
return self.first_name + " " + self.last_name
|
||||||
|
|
||||||
|
def valid_user(self) -> bool:
|
||||||
|
"""Method to check that the loaded user is valid"""
|
||||||
|
return self.__is_valid
|
||||||
|
|
||||||
|
def __load_user(self) -> None:
|
||||||
|
"""Private method to load the user data from the database"""
|
||||||
|
db_connection = DBConn()
|
||||||
|
# Use the username if the user_id was not provided
|
||||||
|
if not self.__user_id:
|
||||||
|
user_details = db_connection.do_select("users", "row", "*", "username = :username",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"field": "username",
|
||||||
|
"value": self.username
|
||||||
|
}
|
||||||
|
])
|
||||||
|
else:
|
||||||
|
user_details = db_connection.do_select("users", "row", "*", "user_id = :user_id",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"field": "user_id",
|
||||||
|
"value": self.__user_id
|
||||||
|
}
|
||||||
|
])
|
||||||
|
if user_details is not None and len(user_details) > 0:
|
||||||
|
self.__user_id = user_details['user_id']
|
||||||
|
self.username = user_details['username']
|
||||||
|
self.first_name = user_details['user_first_name']
|
||||||
|
self.last_name = user_details['user_last_name']
|
||||||
|
self.email_address = user_details['user_email']
|
||||||
|
self.login_ip = ""
|
||||||
|
self.role = user_details['user_role']
|
||||||
|
self.__is_valid = True
|
||||||
|
self.login_date = int(time.time())
|
||||||
|
|
||||||
|
def login(self, password, ip_address) -> any:
|
||||||
|
"""Method for attempting to log in the current user with the provided password"""
|
||||||
|
result = {"is_valid": False, "status_code": 0, "status_msg": ""}
|
||||||
|
# Check that the user isn't already logged in with this instance
|
||||||
|
if not self.__is_logged_in:
|
||||||
|
if len(password) > 0 and len(ip_address) > 0:
|
||||||
|
# Check that the user doesnt have 5 bad login attempts recently and is not locked out
|
||||||
|
if self.bad_login_attempt_count() < 5:
|
||||||
|
db_connection = DBConn()
|
||||||
|
user_details = db_connection.do_select("users", "row", "user_password, user_salt",
|
||||||
|
"username = :username",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"field": "username",
|
||||||
|
"value": self.username
|
||||||
|
}
|
||||||
|
])
|
||||||
|
if user_details is not None and len(user_details) > 0:
|
||||||
|
stored_password = user_details['user_password']
|
||||||
|
stored_salt = user_details['user_salt']
|
||||||
|
|
||||||
|
# Hash the input password with the users salt from the database
|
||||||
|
password_hash = SHA256Custom.SHA256Custom()
|
||||||
|
password_hash.update((password + stored_salt).encode())
|
||||||
|
|
||||||
|
login_password = password_hash.hexdigest()
|
||||||
|
|
||||||
|
# If the passwords match then login
|
||||||
|
if stored_password == login_password:
|
||||||
|
self.__is_logged_in = True
|
||||||
|
self.login_date = int(time.time())
|
||||||
|
# Log the login attempt for the server records
|
||||||
|
self.log_login_attempt(1, ip_address)
|
||||||
|
result = {"is_valid": True, "status_code": 1, "status_msg": "User Logged in."}
|
||||||
|
else:
|
||||||
|
# Log the login attempt for the server records
|
||||||
|
self.log_login_attempt(-1, ip_address)
|
||||||
|
result['status_msg'] = "Incorrect Password."
|
||||||
|
else:
|
||||||
|
result['status_msg'] = "User not found."
|
||||||
|
else:
|
||||||
|
result['status_msg'] = "Too many tries, please wait."
|
||||||
|
result['status_code'] = -1
|
||||||
|
else:
|
||||||
|
result['status_msg'] = "No Password and/or IP address provided."
|
||||||
|
else:
|
||||||
|
result['status_msg'] = "User already logged in."
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def save_current_changes(self) -> bool:
|
||||||
|
"""Method to save any changes made to the user to the database"""
|
||||||
|
result = False
|
||||||
|
if self.__is_valid:
|
||||||
|
db_connection = DBConn()
|
||||||
|
if db_connection.do_update("users",
|
||||||
|
[{"field": "username", "value": self.username},
|
||||||
|
{"field": "user_first_name", "value": self.first_name},
|
||||||
|
{"field": "user_last_name", "value": self.last_name},
|
||||||
|
{"field": "user_email", "value": self.email_address},
|
||||||
|
{"field": "user_role", "value": self.role}],
|
||||||
|
"user_id = :user_id",
|
||||||
|
[{"field": "user_id", "value": self.__user_id}]):
|
||||||
|
result = True
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def create_user(self, first_name: str, last_name: str, password: str, email_address: str, role="User") -> bool:
|
||||||
|
"""Method to create and save the user in the database"""
|
||||||
|
result = False
|
||||||
|
if not self.__is_valid:
|
||||||
|
if len(password) > 0 and len(email_address) and len(role) > 0:
|
||||||
|
user_salt = "".join(random.choices(string.ascii_uppercase + string.digits, k=10))
|
||||||
|
|
||||||
|
password_hash = SHA256Custom.SHA256Custom()
|
||||||
|
password_hash.update((password + user_salt).encode())
|
||||||
|
|
||||||
|
user_password = password_hash.hexdigest()
|
||||||
|
|
||||||
|
db_connection = DBConn()
|
||||||
|
if db_connection.do_insert("users",
|
||||||
|
[{"field": "username", "value": self.username},
|
||||||
|
{"field": "user_first_name", "value": first_name},
|
||||||
|
{"field": "user_last_name", "value": last_name},
|
||||||
|
{"field": "user_password", "value": user_password},
|
||||||
|
{"field": "user_salt", "value": user_salt},
|
||||||
|
{"field": "user_email", "value": email_address},
|
||||||
|
{"field": "user_role", "value": role},
|
||||||
|
{"field": "user_date_created", "value": get_unix_timestamp()}]):
|
||||||
|
self.__load_user()
|
||||||
|
result = True
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def unlock(self) -> None:
|
||||||
|
"""Method to unlock a user if they have been locked out by bad login attempts"""
|
||||||
|
if self.__is_valid and not self.__is_logged_in:
|
||||||
|
db_connection = DBConn()
|
||||||
|
# It just sets the status values to another failed integer so they no longer get counted
|
||||||
|
db_connection.do_update("login_attempts",
|
||||||
|
[{"field": "login_attempt_status", "value": -2}],
|
||||||
|
"user_id = :user_id AND login_attempt_status = :bad_status",
|
||||||
|
[{"field": "user_id", "value": self.__user_id},
|
||||||
|
{"field": "bad_status", "value": -1}])
|
||||||
|
|
||||||
|
def log_login_attempt(self, status, ip_address) -> None:
|
||||||
|
"""Method for getting the current login attempt count from the database"""
|
||||||
|
if self.__is_valid:
|
||||||
|
db_connection = DBConn()
|
||||||
|
db_connection.do_insert("login_attempts",
|
||||||
|
[{"field": "user_id", "value": self.__user_id},
|
||||||
|
{"field": "login_attempt_ip", "value": ip_address},
|
||||||
|
{"field": "login_attempt_status", "value": status},
|
||||||
|
{"field": "login_attempt_timestamp", "value": get_unix_timestamp()}])
|
||||||
|
|
||||||
|
def bad_login_attempt_count(self) -> int:
|
||||||
|
"""Method to count bad login attempts from the user within the set timeframe"""
|
||||||
|
result = 0
|
||||||
|
if self.__user_id:
|
||||||
|
db_connection = DBConn()
|
||||||
|
attempt_count = db_connection.do_select("login_attempts", "col", "COUNT(login_attempt_id)",
|
||||||
|
"user_id = :user_id AND "
|
||||||
|
"login_attempt_status = :bad_login_status AND "
|
||||||
|
"login_attempt_timestamp BETWEEN :start_time AND :end_time",
|
||||||
|
[{"field": "user_id", "value": self.__user_id},
|
||||||
|
{"field": "bad_login_status", "value": -1},
|
||||||
|
{"field": "start_time", "value": get_unix_timestamp(-300)},
|
||||||
|
{"field": "end_time", "value": get_unix_timestamp()}])
|
||||||
|
|
||||||
|
if attempt_count is not None:
|
||||||
|
result = attempt_count
|
||||||
|
|
||||||
|
return result
|
217
Server/dbSetup.py
Normal file
217
Server/dbSetup.py
Normal file
@ -0,0 +1,217 @@
|
|||||||
|
import Crypto
|
||||||
|
import hashlib
|
||||||
|
import SHA256Custom
|
||||||
|
|
||||||
|
from DBConn import DBConn
|
||||||
|
from User import User
|
||||||
|
|
||||||
|
# This is just a setup and test script to create the initial database and to test out some of the features
|
||||||
|
|
||||||
|
# should create the DB file if missing
|
||||||
|
db_connection = DBConn()
|
||||||
|
|
||||||
|
create_system_settings_sql = "CREATE TABLE IF NOT EXISTS system_settings(" \
|
||||||
|
"setting_name TEXT PRIMARY KEY," \
|
||||||
|
"setting_value TEXT" \
|
||||||
|
");"
|
||||||
|
|
||||||
|
create_user_table_sql = "CREATE TABLE IF NOT EXISTS users (" \
|
||||||
|
"user_id INTEGER PRIMARY KEY," \
|
||||||
|
"username TEXT UNIQUE," \
|
||||||
|
"user_first_name TEXT," \
|
||||||
|
"user_last_name TEXT," \
|
||||||
|
"user_password TEXT NOT NULL," \
|
||||||
|
"user_salt TEXT NOT NULL," \
|
||||||
|
"user_email TEXT NOT NULL," \
|
||||||
|
"user_role TEXT NOT NULL," \
|
||||||
|
"user_date_created INTEGER NOT NULL" \
|
||||||
|
");"
|
||||||
|
|
||||||
|
create_user_login_attempt_table = "CREATE TABLE IF NOT EXISTS login_attempts(" \
|
||||||
|
"login_attempt_id INTEGER PRIMARY KEY," \
|
||||||
|
"user_id INTEGER NOT NULL," \
|
||||||
|
"login_attempt_ip TEXT," \
|
||||||
|
"login_attempt_status INTEGER NOT NULL," \
|
||||||
|
"login_attempt_timestamp INTEGER NOT NULL," \
|
||||||
|
"FOREIGN KEY(user_id) REFERENCES users(user_id)" \
|
||||||
|
");"
|
||||||
|
|
||||||
|
create_session_table = "CREATE TABLE IF NOT EXISTS sessions(" \
|
||||||
|
"session_id TEST PRIMARY KEY," \
|
||||||
|
"session_unique_ref TEXT UNIQUE NOT NULL," \
|
||||||
|
"user_id INTEGER NOT NULL," \
|
||||||
|
"session_ip TEXT NOT NULL," \
|
||||||
|
"session_data TEXT," \
|
||||||
|
"session_state TEXT NOT NULL," \
|
||||||
|
"session_last_action_date INTEGER NOT NULL," \
|
||||||
|
"FOREIGN KEY(user_id) REFERENCES users(user_id)" \
|
||||||
|
");"
|
||||||
|
|
||||||
|
create_raw_mail_table_sql = "CREATE TABLE IF NOT EXISTS raw_mail (" \
|
||||||
|
"raw_id INTEGER PRIMARY KEY," \
|
||||||
|
"from_id INTEGER NOT NULL," \
|
||||||
|
"raw_mail TEXT NOT NULL," \
|
||||||
|
"raw_date_sent INTEGER NOT NULL," \
|
||||||
|
"FOREIGN KEY(from_id) REFERENCES users(user_id)" \
|
||||||
|
");"
|
||||||
|
|
||||||
|
create_mail_table_sql = "CREATE TABLE IF NOT EXISTS mail_store (" \
|
||||||
|
"mail_id INTEGER PRIMARY KEY," \
|
||||||
|
"raw_id INTEGER NOT NULL," \
|
||||||
|
"from_id INTEGER NOT NULL," \
|
||||||
|
"to_id INTEGER," \
|
||||||
|
"mail_subject TEXT NOT NULL," \
|
||||||
|
"mail_body TEXT NOT NULL," \
|
||||||
|
"mail_read TEXT DEFAULT 0," \
|
||||||
|
"mail_date_sent INTEGER NOT NULL," \
|
||||||
|
"FOREIGN KEY(from_id) REFERENCES users(user_id)," \
|
||||||
|
"FOREIGN KEY(to_id) REFERENCES users(user_id)" \
|
||||||
|
");"
|
||||||
|
|
||||||
|
create_server_log_sql = "CREATE TABLE IF NOT EXISTS server_logs(" \
|
||||||
|
"log_id INTEGER PRIMARY KEY," \
|
||||||
|
"session_ref TEXT," \
|
||||||
|
"log_action_dir TEXT," \
|
||||||
|
"log_action TEXT," \
|
||||||
|
"log_client_ip TEXT," \
|
||||||
|
"log_date INTEGER" \
|
||||||
|
");"
|
||||||
|
|
||||||
|
# Create the system settings and set the actual system settings needed
|
||||||
|
if db_connection.do_generic(create_system_settings_sql):
|
||||||
|
db_connection.set_system_setting("SERVER_DOMAIN", "server.local")
|
||||||
|
db_connection.set_system_setting("MOTD", "Hello welcome to my SMTP server!")
|
||||||
|
|
||||||
|
help_main_body = "This is the main help text, i cant think of anything to put here,\n"
|
||||||
|
help_main_body += "so i'll fill it with some lorem ipsums, but you can also look at \n"
|
||||||
|
help_main_body += "sub help files using [USEFUL], [LOREM] or [MAIL]."
|
||||||
|
|
||||||
|
db_connection.set_system_setting("HELPMAIN", help_main_body)
|
||||||
|
|
||||||
|
help_lorem_body = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc sit amet arcu tortor. \n" \
|
||||||
|
"Maecenas ante lacus, vehicula id volutpat sed, auctor eget nunc. Mauris id consequat \n" \
|
||||||
|
"augue. Sed molestie diam quis sapien vehicula, non ultricies tellus interdum. \n" \
|
||||||
|
"Pellentesque vel dui enim. Integer sed magna blandit, consectetur felis id, lacinia nisi. \n" \
|
||||||
|
"Phasellus luctus imperdiet orci quis hendrerit. Phasellus vel mattis libero. Suspendisse \n" \
|
||||||
|
"ornare porttitor dolor, eu aliquet quam laoreet at. Integer tincidunt pretium pretium. \n" \
|
||||||
|
"In auctor sapien a erat suscipit, quis semper ex fermentum. Vestibulum bibendum nulla ut \n" \
|
||||||
|
"vestibulum fringilla. Fusce est risus, rhoncus feugiat tellus vitae, feugiat finibus \n" \
|
||||||
|
"lacus. Maecenas gravida ornare imperdiet. Mauris nec est nec mi dapibus dictum viverra \n" \
|
||||||
|
"at libero. Morbi blandit sed lacus a semper."
|
||||||
|
|
||||||
|
db_connection.set_system_setting("HELPLOREM", help_lorem_body)
|
||||||
|
|
||||||
|
help_mail_body = "When you compose a mail message it will take you through 3 sections, the first is to \n" \
|
||||||
|
"collect the email addresses you wish to send to, \n" \
|
||||||
|
"leaving the prompt blank will continue to the next section.\n" \
|
||||||
|
"The next section will ask you to enter a subject for your email, this is straight forward.\n" \
|
||||||
|
"The last section will allow you to enter the body of the email, enter will start a new \n" \
|
||||||
|
"paragraph, if you leave the input blank and hit enter the email will attempt to send."
|
||||||
|
|
||||||
|
db_connection.set_system_setting("HELPMAIL", help_mail_body)
|
||||||
|
|
||||||
|
help_useful_body = "Test Email account email address list:\n" \
|
||||||
|
"admin@server.local,\n" \
|
||||||
|
"testuser@server.local,\n" \
|
||||||
|
"craig.smith@server.local,\n" \
|
||||||
|
"g.lemon@server.local,\n" \
|
||||||
|
"v.king@server.local,\n" \
|
||||||
|
"j.chong@server.local" \
|
||||||
|
|
||||||
|
db_connection.set_system_setting("HELPUSEFUL", help_useful_body)
|
||||||
|
|
||||||
|
# Create the user table and generate the admin and some fake users
|
||||||
|
if db_connection.do_generic(create_user_table_sql):
|
||||||
|
if db_connection.do_generic(create_user_login_attempt_table) and db_connection.do_generic(create_session_table):
|
||||||
|
|
||||||
|
# Set up users and do some class tests
|
||||||
|
admin_user = User("admin")
|
||||||
|
if admin_user.create_user("Jeff", "Jones", "admin", "admin@server.local", "Admin"):
|
||||||
|
print("Admin user created.")
|
||||||
|
|
||||||
|
normal_user = User("testuser")
|
||||||
|
if normal_user.create_user("Adam", "David", "password", "testuser@server.local"):
|
||||||
|
print("Test user created.")
|
||||||
|
|
||||||
|
user_1 = User("user1")
|
||||||
|
if user_1.create_user("Craig", "Smith", "user", "craig.smith@server.local"):
|
||||||
|
print("Test user created.")
|
||||||
|
|
||||||
|
user_2 = User("user2")
|
||||||
|
if user_2.create_user("Greg", "Lemon", "user", "g.lemon@server.local"):
|
||||||
|
print("Test user created.")
|
||||||
|
|
||||||
|
user_3 = User("user3")
|
||||||
|
if user_3.create_user("Vadim", "King", "user", "v.king@server.local"):
|
||||||
|
print("Test user created.")
|
||||||
|
|
||||||
|
user_4 = User("user4")
|
||||||
|
if user_4.create_user("Jim", "Chong", "user", "j.chong@server.local"):
|
||||||
|
print("Test user created.")
|
||||||
|
|
||||||
|
print(admin_user.__dict__)
|
||||||
|
print(normal_user.__dict__)
|
||||||
|
|
||||||
|
user_test = User("admin")
|
||||||
|
for i in range(0, 5):
|
||||||
|
print(user_test.login("nothing", "127.0.0.1"))
|
||||||
|
print(f"Attempt {i+1}, count: {user_test.bad_login_attempt_count()}\n")
|
||||||
|
|
||||||
|
print(user_test.login("admin", "127.0.0.1"))
|
||||||
|
print(f"Attempt 6, count: {user_test.bad_login_attempt_count()}\n")
|
||||||
|
|
||||||
|
print("Unlocking the user account\n")
|
||||||
|
user_test.unlock()
|
||||||
|
|
||||||
|
print(user_test.login("admin", "127.0.0.1"))
|
||||||
|
print(f"Attempt 7, count: {user_test.bad_login_attempt_count()}\n")
|
||||||
|
print(user_test.__dict__)
|
||||||
|
|
||||||
|
user_test.email_address = "admin@email.com"
|
||||||
|
user_test.save_current_changes()
|
||||||
|
print(user_test.__dict__)
|
||||||
|
|
||||||
|
user_test.email_address = "admin@server.local"
|
||||||
|
user_test.save_current_changes()
|
||||||
|
|
||||||
|
# Create the mail store and raw mail tables for holding the emails
|
||||||
|
if db_connection.do_generic(create_raw_mail_table_sql):
|
||||||
|
if db_connection.do_generic(create_mail_table_sql):
|
||||||
|
if db_connection.do_generic(create_server_log_sql):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Testing the Encrypting and RSA class
|
||||||
|
crypto_test = Crypto.Receiver()
|
||||||
|
crypto_test.generate_keys()
|
||||||
|
|
||||||
|
print(f"Public: {crypto_test.get_public_key_pair()}")
|
||||||
|
print(f"Private: {crypto_test.get_private_key_pair()}")
|
||||||
|
|
||||||
|
decrypto_test = Crypto.Transmitter(crypto_test.get_public_key_pair())
|
||||||
|
|
||||||
|
print(f"Public Sent: {crypto_test.get_public_key_pair(False)}")
|
||||||
|
print(f"Public Recv: {decrypto_test.get_current_key_pair()}")
|
||||||
|
|
||||||
|
print("String in: Hello world!")
|
||||||
|
enc_string = decrypto_test.encrypt_string(b"Hello world!")
|
||||||
|
print(f"Encrypted String: {enc_string}")
|
||||||
|
|
||||||
|
dec_string = crypto_test.decrypt_string(enc_string.decode())
|
||||||
|
print(f"Decrypted String: {dec_string}")
|
||||||
|
|
||||||
|
hash_test_string = "Hello World!"
|
||||||
|
|
||||||
|
py_hash = hashlib.sha256(hash_test_string.encode())
|
||||||
|
hash_test_a = py_hash.hexdigest()
|
||||||
|
|
||||||
|
custom_hash = SHA256Custom.SHA256Custom(hash_test_string.encode())
|
||||||
|
hash_test_b = custom_hash.hexdigest()
|
||||||
|
|
||||||
|
print(f"Input string: {hash_test_string}")
|
||||||
|
print(f"Python hash: {hash_test_a}")
|
||||||
|
print(f"Custom hash: {hash_test_b}")
|
||||||
|
|
||||||
|
if hash_test_a == hash_test_b:
|
||||||
|
print("Matching Hash")
|
||||||
|
else:
|
||||||
|
print("Non matching Hash")
|
BIN
Server/server.db
Normal file
BIN
Server/server.db
Normal file
Binary file not shown.
33
Server/server.py
Normal file
33
Server/server.py
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
from SMTPServer import SMTPServer
|
||||||
|
|
||||||
|
server_host = "0.0.0.0"
|
||||||
|
server_port = 50000
|
||||||
|
|
||||||
|
start_attempts = 0
|
||||||
|
max_start_attempts = 3
|
||||||
|
server_started = False
|
||||||
|
quit_flag = False
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
while not quit_flag:
|
||||||
|
print(f"Starting the server on {server_host}:{server_port}")
|
||||||
|
|
||||||
|
smtp_server = SMTPServer(server_host, server_port)
|
||||||
|
|
||||||
|
# Give the server 3 attempts to start, otherwise exit the program
|
||||||
|
if not server_started:
|
||||||
|
server_started = smtp_server.start_server()
|
||||||
|
start_attempts += 1
|
||||||
|
|
||||||
|
if server_started:
|
||||||
|
# Open the server and start the main script loop
|
||||||
|
smtp_server.open_server()
|
||||||
|
if not smtp_server.active:
|
||||||
|
quit_flag = True
|
||||||
|
|
||||||
|
else:
|
||||||
|
if start_attempts == max_start_attempts:
|
||||||
|
print("Could not start the server, Exiting...")
|
||||||
|
quit_flag = True
|
||||||
|
|
||||||
|
print("Server Ended, Thank you.")
|
Reference in New Issue
Block a user