Files
phython-mimic-smtp-clientse…/Server/ServerThread.py
2025-07-05 14:24:22 +01:00

743 lines
43 KiB
Python

# 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