uaveiro-leci/1ano/2semestre/labi/labi2023-ap-g6/client_server/server.py

385 lines
14 KiB
Python
Raw Normal View History

#!/usr/bin/python3
import sys
import socket
import select
import json
import base64
import csv
import random
from common_comm import send_dict, recv_dict, sendrecv_dict
from Crypto.Cipher import AES
from Crypto.Hash import SHA256
# Dicionário com a informação relativa aos clientes
users = {}
# return the client_id of a socket or None
def find_client_id(client_sock):
for client_id in users:
if users[client_id]["socket"] == client_sock:
return client_id
return None
# Função para encriptar valores a enviar em formato json com codificação base64
# return int data encrypted in a 16 bytes binary string and coded base64
def encrypt_intvalue(client_id, data_arg):
key = base64.b64decode(users[client_id]["cipher"])
cipher = AES.new(key, AES.MODE_ECB)
data = cipher.encrypt(bytes("%16d" % data_arg, "utf8"))
return str(base64.b64encode(data), "utf8")
# Função para desencriptar valores recebidos em formato json com codificação base64
# return int data decrypted from a 16 bytes binary string and coded base64
def decrypt_intvalue(client_id, data_arg):
key = base64.b64decode(users[client_id]["cipher"])
cipher = AES.new(key, AES.MODE_ECB)
data = base64.b64decode(data_arg)
data = cipher.decrypt(data)
return int(str(data, "utf8"))
# Função auxiliar para gerar o resultado - já está implementada
# return int value and list of description strings identifying the characteristic of the value
def generate_result(list_values):
if len(list_values) % 2 == 1:
test = 4
else:
test = 3
minimal = min(list_values)
maximal = max(list_values)
first = list_values[0]
last = list_values[-1]
choice = random.randint(0, test)
if choice == 0:
if minimal == first:
return first, ["min", "first"]
elif maximal == first:
return first, ["max", "first"]
else:
return first, ["first"]
elif choice == 1:
if minimal == last:
return last, ["min", "last"]
elif maximal == last:
return last, ["max", "last"]
else:
return last, ["last"]
elif choice == 2:
if minimal == first:
return first, ["min", "first"]
elif minimal == last:
return last, ["min", "last"]
else:
return minimal, ["min"]
elif choice == 3:
if maximal == first:
return first, ["max", "first"]
elif maximal == last:
return last, ["max", "last"]
else:
return maximal, ["max"]
elif choice == 4:
list_values.sort()
median = list_values[len(list_values) // 2]
if median == first:
return first, ["median", "first"]
elif median == last:
return last, ["median", "last"]
else:
return median, ["median"]
else:
return None
# Incomming message structure:
# { op = "START", client_id, [cipher] }
# { op = "QUIT" }
# { op = "NUMBER", number }
# { op = "STOP", [shasum] }
# { op = "GUESS", choice }
#
# Outcomming message structure:
# { op = "START", status }
# { op = "QUIT" , status }
# { op = "NUMBER", status }
# { op = "STOP", status, value }
# { op = "GUESS", status, result }
#
# Suporte de descodificação da operação pretendida pelo cliente - já está implementada
#
def new_msg(client_sock):
request = recv_dict(client_sock)
# print( "Command: %s" % (str(request)) )
op = request["op"]
if op == "START":
response = new_client(client_sock, request)
elif op == "QUIT": #
response = quit_client(client_sock, request)
elif op == "NUMBER": #
response = number_client(client_sock, request)
elif op == "STOP": #
response = stop_client(client_sock, request)
elif op == "GUESS": #
response = guess_client(client_sock, request)
else:
response = {"op": op, "status": False, "error": "Invalid operation"}
# print (response)
send_dict(client_sock, response)
#
# Suporte da criação de um novo cliente - operação START
#
# detect the client in the request
# verify the appropriate conditions for executing this operation
# process the client in the dictionary
# return response message with or without error message
def new_client(client_sock, request):
client_id = request["client_id"]
# check if the client_id is in users
if client_id in users:
response = {"op": "START", "status": False, "error": "Client already exists"}
print("Failed to add client %s\nReason: %s" % (client_id, response["error"]))
else:
cipher = None
# verify if client wants to use cipher
if request["cipher"] is not None:
cipher = request["cipher"]
users[client_id] = {"socket": client_sock, "cipher": cipher, "numbers": [], "has_stopped": False}
response = {"op": "START", "status": True}
print("Client %s added\n" % client_id)
return response
#
# Suporte da eliminação de um cliente - já está implementada
#
# obtain the client_id from his socket and delete from the dictionary
def clean_client(client_sock):
client_id = find_client_id(client_sock)
# check if the client_id is in users
if client_id is not None:
print("Client %s removed\n" % client_id)
del users[client_id]
#
# Suporte do pedido de desistência de um cliente - operação QUIT
#
# obtain the client_id from his socket
# verify the appropriate conditions for executing this operation
# process the report file with the QUIT result
# eliminate client from dictionary using the function clean_client
# return response message with or without error message
def quit_client(client_sock, request):
client_id = find_client_id(client_sock)
# check if the client_id is in users
if client_id is None:
response = {"op": "QUIT", "status": False, "error": "Client does not exist"}
print("Failed to remove client %s\nReason: %s" % (client_id, response["error"]))
else:
# remove client
clean_client(client_sock)
response = {"op": "QUIT", "status": True}
return response
#
# Suporte da criação de um ficheiro csv com o respetivo cabeçalho - já está implementada
#
def create_file():
with open("result.csv", "w", newline="") as csvfile:
columns = ["client_id", "number_of_numbers", "guess"]
fw = csv.DictWriter(csvfile, delimiter=",", fieldnames=columns)
fw.writeheader()
#
# Suporte da actualização de um ficheiro csv com a informação do cliente
#
# update report csv file with the simulation of the client
def update_file(client_id, size, guess):
with open("result.csv", "a", newline="") as csvfile:
writer = csv.DictWriter(csvfile, delimiter=',', fieldnames=["client_id", "number_of_numbers", "guess"])
writer.writerow({"client_id": client_id, "number_of_numbers": size, "guess": guess})
#
# Suporte do processamento do número de um cliente - operação NUMBER
#
# obtain the client_id from his socket
# verify the appropriate conditions for executing this operation
# return response message with or without error message
def number_client(client_sock, request):
client_id = find_client_id(client_sock)
# check if the client_id is in users
if client_id is None:
response = {"op": "NUMBER", "status": False, "error": "Client does not exist"}
print("Failed to add number to client %s\nReason: %s" % (client_id, response["error"]))
# check if client has stopped adding numbers
elif users[client_id]["has_stopped"]:
response = {"op": "NUMBER", "status": False, "error": "Client has stopped"}
print("Failed to add number to client %s\nReason: %s" % (client_id, response["error"]))
else:
num = request["number"]
# decrypt the number if a cipher is being used
if users[client_id]["cipher"] is not None:
num = decrypt_intvalue(client_id, num)
users[client_id]["numbers"].append(num)
response = {"op": "NUMBER", "status": True}
print("Number %d added to client %s\n" % (num, client_id))
return response
#
# Suporte do pedido de terminação de um cliente - operação STOP
#
# obtain the client_id from his socket
# verify the appropriate conditions for executing this operation
# randomly generate a value to return using the function generate_result
# process the report file with the result
# return response message with result or error message
def stop_client(client_sock, request):
client_id = find_client_id(client_sock)
if client_id is None:
response = {"op": "STOP", "status": False, "error": "Client does not exist"}
print("Failed to stop client %s\nReason: %s" % (client_id, response["error"]))
elif len(users[client_id]["numbers"]) < 1:
response = {"op": "STOP", "status": False, "error": "Client has not yet sent any number"}
print("Failed to stop client %s\nReason: %s" % (client_id, response["error"]))
else:
# creates the synthesis for the list
hasher = SHA256.new()
for number in users[client_id]["numbers"]:
hasher.update(bytes(str(number), "utf8"))
# compares the synthesis of the server and the client to see if they match
if hasher.hexdigest() != request["shasum"]:
response = {"op": "STOP", "status": False, "error": "Server numbers list synthesis doesn't match with client list"}
print("Failed to stop client %s\nReason: %s" % (client_id, response["error"]))
else:
# generates the result
value, solution = generate_result(users[client_id]["numbers"])
# encrypts the value if a cipher is being used
if users[client_id]["cipher"] is not None:
encripted_value = encrypt_intvalue(client_id, value)
else:
encripted_value = value
update_file(client_id, len(users[client_id]["numbers"]), solution)
response = {"op": "STOP", "status": True, "value": encripted_value}
users[client_id]["solution"] = solution
users[client_id]["has_stopped"] = True
print("Client %s stopped\nChosen number: %d\nSolution: %s" % (client_id, value, solution))
return response
#
# Suporte da adivinha de um cliente - operação GUESS
#
# obtain the client_id from his socket
# verify the appropriate conditions for executing this operation
# eliminate client from dictionary
# return response message with result or error message
def guess_client(client_sock, request):
client_id = find_client_id(client_sock)
if client_id is None:
response = {"op": "GUESS", "status": False, "error": "Client does not exist"}
print("Failed to guess client %s\nReason: %s" % (client_id, response["error"]))
elif not users[client_id]["has_stopped"]:
response = {"op": "GUESS", "status": False, "error": "Client has not yet stopped"}
print("Failed to guess client %s\nReason: %s" % (client_id, response["error"]))
else:
choice = request["choice"]
response = {"op": "GUESS", "status": True, "result": choice == users[client_id]["solution"]}
print("Client %s guessed %s\n" % (client_id, choice))
return response
def verify_port(port):
# verify if port is a number
if not port.isdigit():
return {"status": False, "error": "Port must be an integer"}
# verify if port is between 1024 and 65535
if not (1024 <= int(port) <= 65535):
return {"status": False, "error": "Port number must be between 1024 and 65535"}
return {"status": True, "port": int(port)}
def main():
# validate the number of arguments and eventually print error message and exit with error
# verify type of arguments and eventually print error message and exit with error
if len(sys.argv) != 2:
print("Usage: python3 %s <port>" % sys.argv[0])
sys.exit(1)
# obtain the port number
port = sys.argv[1]
verified = verify_port(port)
if not verified["status"]:
print(f"{verified['error']}")
sys.exit(1)
port = verified["port"]
# create the server socket
try:
hostname = socket.gethostbyname(socket.gethostname())
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.bind((hostname, port))
print(f"Server started.\nHostname: {hostname}\nPort: {port}\n-----------------------------------------------------\n")
# handle errors while creating server socket and exit with error
except OSError:
print(f"Failed to create server socket, maybe the port is already in use?\n")
sys.exit(1)
server_socket.listen()
clients = []
create_file()
while True:
try:
available = select.select([server_socket] + clients, [], [])[0]
except ValueError:
# Sockets may have been closed, check for that
for client_sock in clients:
if client_sock.fileno() == -1:
clients.remove(client_sock) # closed
continue # Reiterate select
for client_sock in available:
# New client?
if client_sock is server_socket:
newclient, addr = server_socket.accept()
clients.append(newclient)
# Or an existing client
else:
# See if client sent a message
if len(client_sock.recv(1, socket.MSG_PEEK)) != 0:
# client socket has a message
# print ("server" + str (client_sock))
new_msg(client_sock)
else: # Or just disconnected
clients.remove(client_sock)
clean_client(client_sock)
client_sock.close()
break # Reiterate select
if __name__ == "__main__":
main()