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")
|
Reference in New Issue
Block a user