diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..03d9549 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..1d3ce46 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/.idea/shelf/Uncommitted_changes_before_Checkout_at_2_11_2025_11_52_p__m___Changes_.xml b/.idea/shelf/Uncommitted_changes_before_Checkout_at_2_11_2025_11_52_p__m___Changes_.xml new file mode 100644 index 0000000..ddf1117 --- /dev/null +++ b/.idea/shelf/Uncommitted_changes_before_Checkout_at_2_11_2025_11_52_p__m___Changes_.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/.idea/shelf/Uncommitted_changes_before_Checkout_at_2_11_2025_11_52_p__m___Changes_1.xml b/.idea/shelf/Uncommitted_changes_before_Checkout_at_2_11_2025_11_52_p__m___Changes_1.xml new file mode 100644 index 0000000..1c47b24 --- /dev/null +++ b/.idea/shelf/Uncommitted_changes_before_Checkout_at_2_11_2025_11_52_p__m___Changes_1.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git "a/.idea/shelf/Uncommitted_changes_before_Checkout_at_2_11_2025_11_52_p_\302\240m__[Changes]/shelved.patch" "b/.idea/shelf/Uncommitted_changes_before_Checkout_at_2_11_2025_11_52_p_\302\240m__[Changes]/shelved.patch" new file mode 100644 index 0000000..45dc6a4 --- /dev/null +++ "b/.idea/shelf/Uncommitted_changes_before_Checkout_at_2_11_2025_11_52_p_\302\240m__[Changes]/shelved.patch" @@ -0,0 +1,35 @@ +Index: main/client2.py +IDEA additional info: +Subsystem: com.intellij.openapi.diff.impl.patch.BaseRevisionTextPatchEP +<+>import socket\r\nimport sys\r\nimport re\r\n\r\n\r\nclass FTPClient:\r\n def __init__(self, host, port):\r\n self.host = host\r\n self.port = port\r\n self.control_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\r\n self.control_socket.connect((self.host, self.port))\r\n self.receive_response() # Recibir mensaje de bienvenida\r\n\r\n def receive_response(self):\r\n response = self.control_socket.recv(4096).decode('utf-8')\r\n print(response, end='')\r\n return response\r\n\r\n def send_command(self, command):\r\n self.control_socket.send((command + '\\r\\n').encode('utf-8'))\r\n return self.receive_response()\r\n\r\n def login(self, username, password):\r\n user_response = self.send_command(f'USER {username}')\r\n pass_response = self.send_command(f'PASS {password}')\r\n return user_response, pass_response\r\n\r\n def pasv(self):\r\n response = self.send_command('PASV')\r\n if \"227\" in response: # Respuesta de modo pasivo\r\n # Extraer la dirección IP y el puerto de la respuesta\r\n ip_port = re.search(r'\\((\\d+,\\d+,\\d+,\\d+,\\d+,\\d+)\\)', response).group(1)\r\n ip_parts = list(map(int, ip_port.split(',')))\r\n ip = '.'.join(map(str, ip_parts[:4]))\r\n port = (ip_parts[4] << 8) + ip_parts[5]\r\n return ip, port\r\n else:\r\n raise Exception(\"Error al entrar en modo PASV.\")\r\n\r\n def list_files(self):\r\n ip, port = self.pasv()\r\n data_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\r\n data_socket.connect((ip, port))\r\n list_response = self.send_command('LIST')\r\n data = data_socket.recv(4096).decode('utf-8')\r\n data_socket.close()\r\n print(data)\r\n return list_response, data\r\n\r\n def retr(self, filename):\r\n ip, port = self.pasv()\r\n data_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\r\n data_socket.connect((ip, port))\r\n retr_response = self.send_command(f'RETR {filename}')\r\n with open(filename, 'wb') as file:\r\n while True:\r\n data = data_socket.recv(4096)\r\n if not data:\r\n break\r\n file.write(data)\r\n data_socket.close()\r\n print(f\"Archivo '{filename}' descargado correctamente.\")\r\n return retr_response\r\n\r\n def print_working_directory(self):\r\n response = self.send_command('PWD')\r\n if \"257\" in response: # Respuesta exitosa de PWD\r\n print(response)\r\n return response\r\n else:\r\n raise Exception(\"Error al obtener el directorio actual.\")\r\n\r\n def change_working_directory(self, directory):\r\n response = self.send_command(f'CWD {directory}')\r\n if \"250\" in response: # Respuesta exitosa de CWD\r\n print(f\"Directorio cambiado a '{directory}'.\")\r\n return response\r\n else:\r\n raise Exception(f\"Error al cambiar al directorio '{directory}'.\")\r\n\r\n def rename(self, from_name, to_name):\r\n rnfr_response = self.send_command(f'RNFR {from_name}')\r\n if \"350\" in rnfr_response: # Respuesta exitosa de RNFR\r\n rnto_response = self.send_command(f'RNTO {to_name}')\r\n if \"250\" in rnto_response: # Respuesta exitosa de RNTO\r\n print(f\"Archivo renombrado de '{from_name}' a '{to_name}'.\")\r\n else:\r\n raise Exception(f\"Error al renombrar el archivo a '{to_name}'.\")\r\n else:\r\n raise Exception(f\"Error al renombrar el archivo desde '{from_name}'.\")\r\n return rnfr_response, rnto_response\r\n\r\n def make_directory(self, directory):\r\n response = self.send_command(f'MKD {directory}')\r\n if \"257\" in response: # Respuesta exitosa de MKD\r\n print(f\"Directorio '{directory}' creado correctamente.\")\r\n else:\r\n raise Exception(f\"Error al crear el directorio '{directory}'.\")\r\n return response\r\n\r\n def delete_file(self, filename):\r\n response = self.send_command(f'DELE {filename}')\r\n if \"250\" in response: # Respuesta exitosa de DELE\r\n print(f\"Archivo '{filename}' eliminado correctamente.\")\r\n else:\r\n raise Exception(f\"Error al eliminar el archivo '{filename}'.\")\r\n return response\r\n\r\n def stor(self, local_filepath, remote_filename):\r\n ip, port = self.pasv()\r\n data_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\r\n data_socket.connect((ip, port))\r\n stor_response = self.send_command(f'STOR {remote_filename}')\r\n with open(local_filepath, 'rb') as file:\r\n while True:\r\n data = file.read(4096)\r\n if not data:\r\n break\r\n data_socket.sendall(data)\r\n data_socket.close()\r\n final_response = self.receive_response()\r\n if \"226\" in final_response or \"250\" in final_response:\r\n print(f\"Archivo '{local_filepath}' subido como '{remote_filename}' correctamente.\")\r\n else:\r\n raise Exception(f\"Error al subir el archivo '{local_filepath}'.\")\r\n return stor_response, final_response\r\n\r\n def remove_directory(self, directory):\r\n response = self.send_command(f'RMD {directory}')\r\n if \"250\" in response: # Respuesta exitosa de RMD\r\n print(f\"Directorio '{directory}' eliminado correctamente.\")\r\n else:\r\n raise Exception(f\"Error al eliminar el directorio '{directory}'.\")\r\n return response\r\n\r\n def quit(self):\r\n response = self.send_command('QUIT')\r\n self.control_socket.close()\r\n print(\"Conexión cerrada.\")\r\n return response\r\n\r\ndef manage_requests(client, command, arg1, arg2):\r\n try:\r\n if command == \"LIST\":\r\n list_response, data = client.list_files()\r\n print(list_response, data)\r\n elif command == \"DELE\":\r\n if not arg1:\r\n print(\"Falta el argumento requerido: -a para el comando DELE\")\r\n return\r\n dele_response = client.delete_file(arg1)\r\n print(dele_response)\r\n elif command == \"STOR\":\r\n if not arg1 or not arg2:\r\n print(\"Faltan los argumentos requeridos: -a (archivo local) y -b (nombre remoto) para el comando STOR\")\r\n return\r\n stor_response, final_response = client.stor(arg1, arg2)\r\n print(stor_response, final_response)\r\n\r\n elif command == \"RETR\":\r\n if not arg1:\r\n print(\"Falta el argumento requerido: -a para el comando RETR\")\r\n return\r\n retr_response = client.retr(arg1)\r\n print(retr_response)\r\n elif command == \"PWD\":\r\n pwd_response = client.print_working_directory()\r\n print(pwd_response)\r\n elif command == \"CWD\":\r\n if not arg1:\r\n print(\"Falta el argumento requerido: -a para el comando CWD\")\r\n return\r\n cwd_response = client.change_working_directory(arg1)\r\n print(cwd_response)\r\n elif command == \"RNFR\":\r\n if not arg1 or not arg2:\r\n print(\"Faltan los argumentos requeridos: -a y -b para el comando RNFR\")\r\n return\r\n rnfr_response, rnto_response = client.rename(arg1, arg2)\r\n print(rnfr_response, rnto_response)\r\n elif command == \"MKD\":\r\n if not arg1:\r\n print(\"Falta el argumento requerido: -a para el comando MKD\")\r\n return\r\n mkd_response = client.make_directory(arg1)\r\n print(mkd_response)\r\n elif command == \"RMD\":\r\n if not arg1:\r\n print(\"Falta el argumento requerido: -a para el comando RMD\")\r\n return\r\n rmd_response = client.remove_directory(arg1)\r\n print(rmd_response)\r\n elif command:\r\n print(f\"Comando '{command}' no reconocido.\")\r\n else:\r\n print(\"No se proporcionó ningún comando.\")\r\n except Exception as e:\r\n print(f\"Error durante la ejecución del comando '{command}': {str(e)}\")\r\n\r\n quit_response = client.quit()\r\n print(quit_response)\r\n\r\ndef main():\r\n cant_args = {\r\n \"LIST\": 0,\r\n \"DELE\": 1,\r\n \"STOR\": 2,\r\n \"RETR\": 1,\r\n \"PWD\": 0,\r\n \"CWD\": 1,\r\n \"RNFR\": 2,\r\n \"MKD\": 1,\r\n \"RMD\": 1\r\n }\r\n\r\n print(\"Bienvenido, para salir inserte 'exit'\")\r\n host = input(\"Inserte el host: \")\r\n if host == \"exit\":\r\n return\r\n port = int(input(\"Inserte el puerto: \"))\r\n if port == \"exit\":\r\n return\r\n user = input(\"Inserte el usuario: \")\r\n if user == \"exit\":\r\n return\r\n password = input(\"Inserte la contraseña: \")\r\n if password == \"exit\":\r\n return\r\n while True:\r\n command = input(\"Inserte un comando (DELE, LIST, STOR, RETR, PWD, CWD, RNFR, MKD, RMD): \")\r\n if command == \"exit\":\r\n break\r\n if command not in cant_args:\r\n print(\"Comando no reconocido\")\r\n continue\r\n\r\n args = [\"\",\"\"]\r\n for i in range(cant_args[command]):\r\n arg = input(f\"Inserte el argumento {i+1}: \")\r\n if arg == \"exit\":\r\n break\r\n args[i] = arg\r\n\r\n client = FTPClient(host, port)\r\n user_response, pass_response = client.login(user, password)\r\n manage_requests(client, command, args[0], args[1])\r\n\r\n\r\n\r\n\r\nif __name__ == \"__main__\":\r\n main() +=================================================================== +diff --git a/main/client2.py b/main/client2.py +--- a/main/client2.py (revision f6eab3492ec0949eb00ee7dcc887c9314a37b5d9) ++++ b/main/client2.py (date 1739335858444) +@@ -43,7 +43,7 @@ + data_socket.connect((ip, port)) + list_response = self.send_command('LIST') + data = data_socket.recv(4096).decode('utf-8') +- data_socket.close() ++ + print(data) + return list_response, data + +@@ -58,7 +58,7 @@ + if not data: + break + file.write(data) +- data_socket.close() ++ + print(f"Archivo '{filename}' descargado correctamente.") + return retr_response + +@@ -117,7 +117,7 @@ + if not data: + break + data_socket.sendall(data) +- data_socket.close() ++ + final_response = self.receive_response() + if "226" in final_response or "250" in final_response: + print(f"Archivo '{local_filepath}' subido como '{remote_filename}' correctamente.") diff --git "a/.idea/shelf/Uncommitted_changes_before_Checkout_at_2_11_2025_11_52_p_\302\240m__[Changes]1/shelved.patch" "b/.idea/shelf/Uncommitted_changes_before_Checkout_at_2_11_2025_11_52_p_\302\240m__[Changes]1/shelved.patch" new file mode 100644 index 0000000..2399210 --- /dev/null +++ "b/.idea/shelf/Uncommitted_changes_before_Checkout_at_2_11_2025_11_52_p_\302\240m__[Changes]1/shelved.patch" @@ -0,0 +1,219 @@ +Index: main/server.py +IDEA additional info: +Subsystem: com.intellij.openapi.diff.impl.patch.BaseRevisionTextPatchEP +<+>import socket\r\nimport os\r\nimport threading\r\n\r\n\r\nclass FTPServer:\r\n def __init__(self, host, port):\r\n self.host = host\r\n self.port = port\r\n self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\r\n self.server_socket.bind((self.host, self.port))\r\n self.server_socket.listen(5)\r\n print(f\"Servidor FTP escuchando en {self.host}:{self.port}\")\r\n\r\n def handle_client(self, client_socket):\r\n client_socket.send(\"220 Bienvenido al Servidor FTP\\r\\n\".encode('utf-8'))\r\n while True:\r\n data = client_socket.recv(1024).decode('utf-8').strip()\r\n if not data:\r\n break\r\n\r\n command = data.split(' ')[0].upper()\r\n if command == 'USER':\r\n client_socket.send(\"331 Nombre de usuario correcto, se requiere contraseña\\r\\n\".encode('utf-8'))\r\n elif command == 'PASS':\r\n client_socket.send(\"230 Usuario autenticado, proceda\\r\\n\".encode('utf-8'))\r\n elif command == 'LIST':\r\n files = os.listdir('.')\r\n client_socket.send(\"150 Listado de directorio en proceso\\r\\n\".encode('utf-8'))\r\n client_socket.send(('\\r\\n'.join(files) + '\\r\\n').encode('utf-8'))\r\n client_socket.send(\"226 Listado de directorio completado\\r\\n\".encode('utf-8'))\r\n elif command == 'QUIT':\r\n client_socket.send(\"221 Adios\\r\\n\".encode('utf-8'))\r\n break\r\n else:\r\n client_socket.send(\"500 Comando desconocido\\r\\n\".encode('utf-8'))\r\n\r\n client_socket.close()\r\n\r\n def run(self):\r\n while True:\r\n client_socket, addr = self.server_socket.accept()\r\n print(f\"Conexión aceptada de {addr}\")\r\n client_thread = threading.Thread(target=self.handle_client, args=(client_socket,))\r\n client_thread.start()\r\n\r\n\r\nif __name__ == \"__main__\":\r\n server = FTPServer('0.0.0.0', 21)\r\n server.run()\r\n +Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP +<+>UTF-8 +=================================================================== +diff --git a/main/server.py b/main/server.py +--- a/main/server.py (revision f6eab3492ec0949eb00ee7dcc887c9314a37b5d9) ++++ b/main/server.py (date 1739332619204) +@@ -19,7 +19,9 @@ + if not data: + break + +- command = data.split(' ')[0].upper() ++ command, *args = data.split(' ') ++ command = command.upper() ++ + if command == 'USER': + client_socket.send("331 Nombre de usuario correcto, se requiere contraseña\r\n".encode('utf-8')) + elif command == 'PASS': +@@ -29,6 +31,65 @@ + client_socket.send("150 Listado de directorio en proceso\r\n".encode('utf-8')) + client_socket.send(('\r\n'.join(files) + '\r\n').encode('utf-8')) + client_socket.send("226 Listado de directorio completado\r\n".encode('utf-8')) ++ elif command == 'RETR': ++ filename = args[0] ++ if os.path.isfile(filename): ++ client_socket.send("150 Apertura de conexión de datos para transferencia\r\n".encode('utf-8')) ++ with open(filename, 'rb') as file: ++ client_socket.sendfile(file) ++ client_socket.send("226 Transferencia de archivo completada\r\n".encode('utf-8')) ++ else: ++ client_socket.send("550 Archivo no encontrado\r\n".encode('utf-8')) ++ elif command == 'STOR': ++ filename = args[0] ++ client_socket.send("150 Apertura de conexión de datos para transferencia\r\n".encode('utf-8')) ++ with open(filename, 'wb') as file: ++ while True: ++ data = client_socket.recv(1024) ++ if not data: ++ break ++ file.write(data) ++ client_socket.send("226 Transferencia de archivo completada\r\n".encode('utf-8')) ++ elif command == 'PWD': ++ client_socket.send(f'257 "{os.getcwd()}"\r\n'.encode('utf-8')) ++ elif command == 'CWD': ++ directory = args[0] ++ try: ++ os.chdir(directory) ++ client_socket.send("250 Cambio de directorio exitoso\r\n".encode('utf-8')) ++ except FileNotFoundError: ++ client_socket.send("550 Directorio no encontrado\r\n".encode('utf-8')) ++ elif command == 'MKD': ++ directory = args[0] ++ try: ++ os.mkdir(directory) ++ client_socket.send(f'257 "{directory}" creado\r\n'.encode('utf-8')) ++ except FileExistsError: ++ client_socket.send("550 El directorio ya existe\r\n".encode('utf-8')) ++ elif command == 'RMD': ++ directory = args[0] ++ try: ++ os.rmdir(directory) ++ client_socket.send("250 Directorio eliminado\r\n".encode('utf-8')) ++ except FileNotFoundError: ++ client_socket.send("550 Directorio no encontrado\r\n".encode('utf-8')) ++ elif command == 'DELE': ++ filename = args[0] ++ try: ++ os.remove(filename) ++ client_socket.send("250 Archivo eliminado\r\n".encode('utf-8')) ++ except FileNotFoundError: ++ client_socket.send("550 Archivo no encontrado\r\n".encode('utf-8')) ++ elif command == 'RNFR': ++ self.rename_from = args[0] ++ client_socket.send("350 Listo para recibir el nuevo nombre\r\n".encode('utf-8')) ++ elif command == 'RNTO': ++ new_name = args[0] ++ try: ++ os.rename(self.rename_from, new_name) ++ client_socket.send("250 Cambio de nombre exitoso\r\n".encode('utf-8')) ++ except FileNotFoundError: ++ client_socket.send("550 Archivo no encontrado\r\n".encode('utf-8')) + elif command == 'QUIT': + client_socket.send("221 Adios\r\n".encode('utf-8')) + break +@@ -46,5 +107,5 @@ + + + if __name__ == "__main__": +- server = FTPServer('0.0.0.0', 21) +- server.run() ++ server = FTPServer('127.0.0.1', 21) ++ server.run() +\ No newline at end of file +Index: main/server2.py +=================================================================== +diff --git a/main/server2.py b/main/server2.py +new file mode 100644 +--- /dev/null (date 1739334676076) ++++ b/main/server2.py (date 1739334676076) +@@ -0,0 +1,115 @@ ++import socket ++import os ++import re ++import threading ++ ++class FTPServer: ++ def __init__(self, host, port): ++ self.host = host ++ self.port = port ++ self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) ++ self.server_socket.bind((self.host, self.port)) ++ self.server_socket.listen(5) ++ self.current_directory = os.getcwd() ++ print(f"Servidor FTP escuchando en {self.host}:{self.port}") ++ ++ def start(self): ++ while True: ++ client_socket, client_address = self.server_socket.accept() ++ print(f"Conexión establecida con {client_address}") ++ threading.Thread(target=self.handle_client, args=(client_socket,)).start() ++ ++ def handle_client(self, client_socket): ++ client_socket.send(f"220 Servidor FTP listo.\r\n") ++ while True: ++ try: ++ command = client_socket.recv(4096).decode('utf-8').strip() ++ if not command: ++ break ++ ++ if command.startswith("USER"): ++ client_socket.send(f"331 Usuario OK, necesita contraseña.") ++ elif command.startswith("PASS"): ++ client_socket.send(f"230 Usuario autenticado.\r\n") ++ elif command.startswith("PASV"): ++ ip = self.host.replace('.', ',') ++ port = 20000 # Puerto arbitrario para el modo pasivo ++ client_socket.send(f"227 Entering Passive Mode ({ip},{port >> 8},{port & 0xff}).\r\n".encode('utf-8')) ++ elif command.startswith("LIST"): ++ client_socket.send(f"150 Abriendo conexión de datos.\r\n") ++ data_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) ++ data_socket.connect((self.host, 20000)) ++ files = "\r\n".join(os.listdir(self.current_directory)) ++ data_socket.send(files.encode('utf-8')) ++ data_socket.close() ++ client_socket.send(f"226 Transferencia completada.\r\n") ++ elif command.startswith("RETR"): ++ filename = command.split(' ')[1] ++ if os.path.exists(filename): ++ client_socket.send(f"150 Abriendo conexión de datos.\r\n") ++ data_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) ++ data_socket.connect((self.host, 20000)) ++ with open(filename, 'rb') as file: ++ data_socket.sendfile(file) ++ data_socket.close() ++ client_socket.send(f"226 Transferencia completada.\r\n") ++ else: ++ client_socket.send(f"550 Archivo no encontrado.\r\n") ++ elif command.startswith("STOR"): ++ filename = command.split(' ')[1] ++ client_socket.send(f"150 Abriendo conexión de datos.\r\n") ++ data_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) ++ data_socket.connect((self.host, 20000)) ++ with open(filename, 'wb') as file: ++ while True: ++ data = data_socket.recv(4096) ++ if not data: ++ break ++ file.write(data) ++ data_socket.close() ++ client_socket.send(f"226 Transferencia completada.\r\n") ++ elif command.startswith("PWD"): ++ client_socket.send(f"257 \"{self.current_directory}\"\r\n".encode('utf-8')) ++ elif command.startswith("CWD"): ++ directory = command.split(' ')[1] ++ if os.path.isdir(directory): ++ self.current_directory = directory ++ client_socket.send(f"250 Directorio cambiado.\r\n") ++ else: ++ client_socket.send(f"550 Directorio no encontrado.\r\n") ++ elif command.startswith("RNFR"): ++ client_socket.send(f"350 Esperando nuevo nombre.\r\n") ++ elif command.startswith("RNTO"): ++ client_socket.send(f"250 Archivo renombrado.\r\n") ++ elif command.startswith("MKD"): ++ directory = command.split(' ')[1] ++ os.mkdir(directory) ++ client_socket.send(f"257 Directorio creado.\r\n") ++ elif command.startswith("DELE"): ++ filename = command.split(' ')[1] ++ if os.path.exists(filename): ++ os.remove(filename) ++ client_socket.send(f"250 Archivo eliminado.\r\n") ++ else: ++ client_socket.send(f"550 Archivo no encontrado.\r\n") ++ elif command.startswith("RMD"): ++ directory = command.split(' ')[1] ++ if os.path.isdir(directory): ++ os.rmdir(directory) ++ client_socket.send(f"250 Directorio eliminado.\r\n") ++ else: ++ client_socket.send(f"550 Directorio no encontrado.\r\n") ++ elif command.startswith("QUIT"): ++ client_socket.send(f"221 Adios.\r\n") ++ client_socket.close() ++ break ++ else: ++ client_socket.send(f"500 Comando no reconocido.\r\n") ++ except Exception as e: ++ print(f"Error: {e}") ++ client_socket.send(f"500 Error en el servidor.\r\n") ++ break ++ ++if __name__ == "__main__": ++ server = FTPServer("127.0.0.1", 2121) ++ server.start() +\ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml new file mode 100644 index 0000000..fa9d88f --- /dev/null +++ b/.idea/workspace.xml @@ -0,0 +1,202 @@ + + + + + + + + + + + + + + + + + { + "lastFilter": {} +} + { + "prStates": [ + { + "id": { + "id": "PR_kwDONfb2is6Kj5Ux", + "number": 2 + }, + "lastSeen": 1739321171011 + } + ] +} + { + "selectedUrlAndAccountId": { + "url": "https://github.com/AdrianSouto/computer_networks_fall_2024.git", + "accountId": "152805fa-e379-450f-81e9-6a178c69903e" + } +} + { + "associatedIndex": 6 +} + + + + { + "keyToString": { + "Python.client.executor": "Run", + "Python.server.executor": "Run", + "RunOnceActivity.ShowReadmeOnStart": "true", + "git-widget-placeholder": "main", + "last_opened_file_path": "D:/Escuela/3er Año/Redes/computer_networks_fall_2024", + "node.js.detected.package.eslint": "true", + "node.js.detected.package.tslint": "true", + "node.js.selected.package.eslint": "(autodetect)", + "node.js.selected.package.tslint": "(autodetect)", + "nodejs_package_manager_path": "npm", + "vue.rearranger.settings.migration": "true" + } +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1739318957045 + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/README.md b/README.md index 26e6bf0..bccbad4 100644 --- a/README.md +++ b/README.md @@ -1 +1,62 @@ -# computer_networks_fall_2024 \ No newline at end of file +# Repositorio para la entrega de proyectos de la asignatura de Redes de Computadoras. Otoño 2024 - 2025 + +### Requisitos para la ejecución de las pruebas: + +1. Ajustar la variable de entorno `procotol` dentro del archivo `env.sh` al protocolo correspondiente. + +2. Modificar el archivo `run.sh` con respecto a la ejecución de la solución propuesta. + +### Ejecución de los tests: + +1. En cada fork del proyecto principal, en el apartado de `actions` se puede ejecutar de forma manual la verificación del código propuesto. + +2. Abrir un `pull request` en el repo de la asignatura a partir de la propuesta con la solución. + +### Descripción general del funcionamineto de las pruebas: + +Todas las pruebas siguen un modelo de ejecución simple. Cada comprobación ejecuta un llamado al scrip `run.sh` contenido en la raíz del proyecto, inyectando los parametros correspondientes. + +La forma de comprobación es similar a todos los protocolos y se requiere que el ejecutable provisto al script `run.sh` sea capaz de, en cada llamado, invocar el método o argumento provisto y terminar la comunicación tras la ejecución satisfactoria del metodo o funcionalidad provista. + +### Argumentos provistos por protocolo: + +#### HTTP: +1. -m method. Ej. `GET` +2. -u url. Ej `http://localhost:4333/example` +3. -h header. Ej `{}` o `{"User-Agent": "device"}` +4. -d data. Ej `Body content` + +#### SMTP: +1. -p port. Ej. `25` +2. -u host. Ej `127.0.0.1` +3. -f from_mail. Ej. `user1@uh.cu` +4. -f to_mail. Ej. `["user2@uh.cu", "user3@uh.cu"]` +5. -s subject. Ej `"Email for testing purposes"` +6. -b body. Ej `"Body content"` +7. -h header. Ej `{}` o ```{"CC": "cc@examplecom"}``` + +#### FTP: +1. -p port. Ej. `21` +2. -h host. Ej `127.0.0.1` +3. -u user. Ej. `user` +4. -w pass. Ej. `pass` +5. -c command. Ej `STOR` +6. -a first argument. Ej `"tests/ftp/new.txt"` +7. -b second argument. Ej `"new.txt"` + +#### IRC +1. -p port. Ej. `8080` +2. -H host. Ej `127.0.0.1` +3. -n nick. Ej. `TestUser1` +4. -c command. Ej `/nick` +5. -a argument. Ej `"NewNick"` + +### Comportamiento de la salida esperada por cada protocolo: + +1. ``HTTP``: Json con formato ```{"status": 200, "body": "server output"}``` + +2. ``SMTP``: Json con formato ```{"status_code": 333, "message": "server output"}``` + +3. ``FTP``: Salida Unificada de cada interacción con el servidor. + +4. ``IRC``: Salida Unificada de cada interacción con el servidor. diff --git a/env.sh b/env.sh index 3f6ac27..001beb5 100644 --- a/env.sh +++ b/env.sh @@ -7,7 +7,7 @@ # 3. SMTP # 4. IRC -PROTOCOL=0 +PROTOCOL=2 # Don't modify the next line -echo "PROTOCOL=${PROTOCOL}" >> "$GITHUB_ENV" \ No newline at end of file +echo "PROTOCOL=${PROTOCOL}" >> "$GITHUB_ENV" diff --git a/main/client.py b/main/client.py new file mode 100644 index 0000000..5eece40 --- /dev/null +++ b/main/client.py @@ -0,0 +1,231 @@ +import socket +import sys +import re + + +class FTPClient: + def __init__(self, host, port): + self.host = host + self.port = port + self.control_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.control_socket.connect((self.host, self.port)) + self.receive_response() # Recibir mensaje de bienvenida + + def receive_response(self): + response = self.control_socket.recv(4096).decode('utf-8') + print(response, end='') + return response + + def send_command(self, command): + self.control_socket.send((command + '\r\n').encode('utf-8')) + return self.receive_response() + + def login(self, username, password): + user_response = self.send_command(f'USER {username}') + pass_response = self.send_command(f'PASS {password}') + return user_response, pass_response + + def pasv(self): + response = self.send_command('PASV') + if "227" in response: # Respuesta de modo pasivo + # Extraer la dirección IP y el puerto de la respuesta + ip_port = re.search(r'\((\d+,\d+,\d+,\d+,\d+,\d+)\)', response).group(1) + ip_parts = list(map(int, ip_port.split(','))) + ip = '.'.join(map(str, ip_parts[:4])) + port = (ip_parts[4] << 8) + ip_parts[5] + return ip, port + else: + raise Exception("Error al entrar en modo PASV.") + + def list_files(self): + ip, port = self.pasv() + data_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + data_socket.connect((ip, port)) + list_response = self.send_command('LIST') + data = data_socket.recv(4096).decode('utf-8') + data_socket.close() + print(data) + return list_response, data + + def retr(self, filename): + ip, port = self.pasv() + data_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + data_socket.connect((ip, port)) + retr_response = self.send_command(f'RETR {filename}') + with open(filename, 'wb') as file: + while True: + data = data_socket.recv(4096) + if not data: + break + file.write(data) + data_socket.close() + print(f"Archivo '{filename}' descargado correctamente.") + return retr_response + + def print_working_directory(self): + response = self.send_command('PWD') + if "257" in response: # Respuesta exitosa de PWD + print(response) + return response + else: + raise Exception("Error al obtener el directorio actual.") + + def change_working_directory(self, directory): + response = self.send_command(f'CWD {directory}') + if "250" in response: # Respuesta exitosa de CWD + print(f"Directorio cambiado a '{directory}'.") + return response + else: + raise Exception(f"Error al cambiar al directorio '{directory}'.") + + def rename(self, from_name, to_name): + rnfr_response = self.send_command(f'RNFR {from_name}') + if "350" in rnfr_response: # Respuesta exitosa de RNFR + rnto_response = self.send_command(f'RNTO {to_name}') + if "250" in rnto_response: # Respuesta exitosa de RNTO + print(f"Archivo renombrado de '{from_name}' a '{to_name}'.") + else: + raise Exception(f"Error al renombrar el archivo a '{to_name}'.") + else: + raise Exception(f"Error al renombrar el archivo desde '{from_name}'.") + return rnfr_response, rnto_response + + def make_directory(self, directory): + response = self.send_command(f'MKD {directory}') + if "257" in response: # Respuesta exitosa de MKD + print(f"Directorio '{directory}' creado correctamente.") + else: + raise Exception(f"Error al crear el directorio '{directory}'.") + return response + + def delete_file(self, filename): + response = self.send_command(f'DELE {filename}') + if "250" in response: # Respuesta exitosa de DELE + print(f"Archivo '{filename}' eliminado correctamente.") + else: + raise Exception(f"Error al eliminar el archivo '{filename}'.") + return response + + def stor(self, local_filepath, remote_filename): + ip, port = self.pasv() + data_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + data_socket.connect((ip, port)) + stor_response = self.send_command(f'STOR {remote_filename}') + with open(local_filepath, 'rb') as file: + while True: + data = file.read(4096) + if not data: + break + data_socket.sendall(data) + data_socket.close() + final_response = self.receive_response() + if "226" in final_response or "250" in final_response: + print(f"Archivo '{local_filepath}' subido como '{remote_filename}' correctamente.") + else: + raise Exception(f"Error al subir el archivo '{local_filepath}'.") + return stor_response, final_response + + def remove_directory(self, directory): + response = self.send_command(f'RMD {directory}') + if "250" in response: # Respuesta exitosa de RMD + print(f"Directorio '{directory}' eliminado correctamente.") + else: + raise Exception(f"Error al eliminar el directorio '{directory}'.") + return response + + def quit(self): + response = self.send_command('QUIT') + self.control_socket.close() + print("Conexión cerrada.") + return response + + +def main(): + # Parsear argumentos + args = {} + for i in range(1, len(sys.argv), 2): + args[sys.argv[i]] = sys.argv[i + 1] + + # Validar argumentos requeridos + required_args = ['-p', '-h', '-u', '-w'] + for arg in required_args: + if arg not in args: + print(f"Falta el argumento requerido: {arg}") + print("Uso: python client.py -p PORT -h HOST -u USER -w PASS -c COMMAND [-a ARG1] [-b ARG2]") + return + + host = args['-h'] + port = int(args['-p']) + user = args['-u'] + password = args['-w'] + command = args.get('-c', '') + arg1 = args.get('-a', '') + arg2 = args.get('-b', '') + + client = FTPClient(host, port) + user_response, pass_response = client.login(user, password) + + try: + if command == "LIST": + list_response, data = client.list_files() + print(list_response, data) + elif command == "DELE": + if not arg1: + print("Falta el argumento requerido: -a para el comando DELE") + return + dele_response = client.delete_file(arg1) + print(dele_response) + elif command == "STOR": + if not arg1 or not arg2: + print("Faltan los argumentos requeridos: -a (archivo local) y -b (nombre remoto) para el comando STOR") + return + stor_response, final_response = client.stor(arg1, arg2) + print(stor_response, final_response) + + elif command == "RETR": + if not arg1: + print("Falta el argumento requerido: -a para el comando RETR") + return + retr_response = client.retr(arg1) + print(retr_response) + elif command == "PWD": + pwd_response = client.print_working_directory() + print(pwd_response) + elif command == "CWD": + if not arg1: + print("Falta el argumento requerido: -a para el comando CWD") + return + cwd_response = client.change_working_directory(arg1) + print(cwd_response) + elif command == "RNFR": + if not arg1 or not arg2: + print("Faltan los argumentos requeridos: -a y -b para el comando RNFR") + return + rnfr_response, rnto_response = client.rename(arg1, arg2) + print(rnfr_response, rnto_response) + elif command == "MKD": + if not arg1: + print("Falta el argumento requerido: -a para el comando MKD") + return + mkd_response = client.make_directory(arg1) + print(mkd_response) + elif command == "RMD": + if not arg1: + print("Falta el argumento requerido: -a para el comando RMD") + return + rmd_response = client.remove_directory(arg1) + print(rmd_response) + elif command: + print(f"Comando '{command}' no reconocido.") + else: + print("No se proporcionó ningún comando.") + except Exception as e: + print(f"Error durante la ejecución del comando '{command}': {str(e)}") + + quit_response = client.quit() + print(quit_response) + + +if __name__ == "__main__": + + main() \ No newline at end of file diff --git a/main/client2.py b/main/client2.py new file mode 100644 index 0000000..7c253f7 --- /dev/null +++ b/main/client2.py @@ -0,0 +1,230 @@ +import socket +import sys +import re + +class FTPClient: + def __init__(self, host, port): + self.host = host + self.port = port + self.control_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.control_socket.connect((self.host, self.port)) + self.receive_response() + + def receive_response(self): + response = '' + while True: + data = self.control_socket.recv(4096).decode('utf-8') + response += data + if self.is_response_complete(response): + break + if not data: + break + print(response, end='') + return response + + def is_response_complete(self, response): + lines = response.split('\r\n') + lines = [line for line in lines if line] + if not lines: + return False + last_line = lines[-1] + + if re.match(r'^\d{3} ', last_line): + return True + else: + return False + + def send_command(self, command): + self.control_socket.sendall((command + '\r\n').encode('utf-8')) + return self.receive_response() + + def login(self, username, password): + user_response = self.send_command(f'USER {username}') + pass_response = self.send_command(f'PASS {password}') + return user_response, pass_response + + def pasv(self): + response = self.send_command('PASV') + if "227" in response: # Respuesta de modo pasivo + # Extraer la dirección IP y el puerto de la respuesta + ip_port = re.search(r'\((\d+,\d+,\d+,\d+,\d+,\d+)\)', response).group(1) + ip_parts = list(map(int, ip_port.split(','))) + ip = '.'.join(map(str, ip_parts[:4])) + port = (ip_parts[4] << 8) + ip_parts[5] + return ip, port + else: + raise Exception("Error al entrar en modo PASV.") + + def list_files(self): + ip, port = self.pasv() + data_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + data_socket.connect((ip, port)) + list_response = self.send_command('LIST') + data = '' + while True: + chunk = data_socket.recv(4096).decode('utf-8') + if not chunk: + break + data += chunk + data_socket.close() + print(data) + return list_response, data + + def retr(self, filename): + ip, port = self.pasv() + data_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + data_socket.connect((ip, port)) + retr_response = self.send_command(f'RETR {filename}') + with open(filename, 'wb') as file: + while True: + data = data_socket.recv(4096) + if not data: + break + file.write(data) + data_socket.close() + print(f"Archivo '{filename}' descargado correctamente.") + return retr_response + + def print_working_directory(self): + response = self.send_command('PWD') + if "257" in response: + print(response) + return response + else: + raise Exception("Error al obtener el directorio actual.") + + def change_working_directory(self, directory): + response = self.send_command(f'CWD {directory}') + if "250" in response: + print(f"Directorio cambiado a '{directory}'.") + return response + else: + raise Exception(f"Error al cambiar al directorio '{directory}'.") + + def rename(self, from_name, to_name): + rnfr_response = self.send_command(f'RNFR {from_name}') + if "350" in rnfr_response: # Respuesta exitosa de RNFR + rnto_response = self.send_command(f'RNTO {to_name}') + if "250" in rnto_response: # Respuesta exitosa de RNTO + print(f"Archivo renombrado de '{from_name}' a '{to_name}'.") + else: + raise Exception(f"Error al renombrar el archivo a '{to_name}'.") + else: + raise Exception(f"Error al renombrar el archivo desde '{from_name}'.") + return rnfr_response, rnto_response + + def make_directory(self, directory): + response = self.send_command(f'MKD {directory}') + if "257" in response: + print(f"Directorio '{directory}' creado correctamente.") + else: + raise Exception(f"Error al crear el directorio '{directory}'.") + return response + + def delete_file(self, filename): + response = self.send_command(f'DELE {filename}') + if "250" in response: + print(f"Archivo '{filename}' eliminado correctamente.") + else: + raise Exception(f"Error al eliminar el archivo '{filename}'.") + return response + + def remove_directory(self, directory): + response = self.send_command(f'RMD {directory}') + if "250" in response: + print(f"Directorio '{directory}' eliminado correctamente.") + else: + raise Exception(f"Error al eliminar el directorio '{directory}'.") + return response + + def stor(self, local_filepath, remote_filename): + ip, port = self.pasv() + data_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + data_socket.connect((ip, port)) + stor_response = self.send_command(f'STOR {remote_filename}') + with open(local_filepath, 'rb') as file: + while True: + data = file.read(4096) + if not data: + break + data_socket.sendall(data) + data_socket.close() + final_response = self.receive_response() + if "226" in final_response or "250" in final_response: + print(f"Archivo '{local_filepath}' subido como '{remote_filename}' correctamente.") + else: + raise Exception(f"Error al subir el archivo '{local_filepath}'.") + return stor_response, final_response + + def quit(self): + response = self.send_command('QUIT') + self.control_socket.close() + print("Conexión cerrada.") + return response + +def main(): + host = input("Ingrese el host: ") + port = int(input("Ingrese el puerto: ")) + user = input("Ingrese el usuario: ") + password = input("Ingrese la contraseña: ") + + client = FTPClient(host, port) + client.login(user, password) + + while True: + try: + command_input = input("ftp> ").strip() + if not command_input: + continue + command_parts = command_input.split() + command = command_parts[0].upper() + + if command == "LIST": + client.list_files() + elif command == "DELE": + if len(command_parts) < 2: + print("Uso: DELE ") + continue + client.delete_file(command_parts[1]) + elif command == "STOR": + if len(command_parts) < 3: + print("Uso: STOR ") + continue + client.stor(command_parts[1], command_parts[2]) + elif command == "RETR": + if len(command_parts) < 2: + print("Uso: RETR ") + continue + client.retr(command_parts[1]) + elif command == "PWD": + client.print_working_directory() + elif command == "CWD": + if len(command_parts) < 2: + print("Uso: CWD ") + continue + client.change_working_directory(command_parts[1]) + elif command == "RNFR": + if len(command_parts) < 3: + print("Uso: RNFR ") + continue + client.rename(command_parts[1], command_parts[2]) + elif command == "MKD": + if len(command_parts) < 2: + print("Uso: MKD ") + continue + client.make_directory(command_parts[1]) + elif command == "RMD": + if len(command_parts) < 2: + print("Uso: RMD ") + continue + client.remove_directory(command_parts[1]) + elif command == "QUIT" or command == "EXIT": + client.quit() + break + else: + print(f"Comando '{command}' no reconocido.") + except Exception as e: + print(f"Error durante la ejecución del comando '{command}': {str(e)}") + +if __name__ == "__main__": + main() diff --git a/main/server.py b/main/server.py new file mode 100644 index 0000000..1608cdf --- /dev/null +++ b/main/server.py @@ -0,0 +1,224 @@ +import socket +import threading +import os +import re + +class FTPServer: + def __init__(self, host='', port=21): + self.host = host + self.port = port + self.server_socket = None + self.working_directory = os.getcwd() + self.users = {'usuario': 'contraseña'} # Diccionario de usuarios para autenticación + + def start(self): + # Crear el socket del servidor + self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.server_socket.bind((self.host, self.port)) + self.server_socket.listen(5) + print(f"Servidor FTP iniciado en {self.host}:{self.port}") + + while True: + client_socket, client_address = self.server_socket.accept() + print(f"Conexión entrante de {client_address}") + client_handler = threading.Thread(target=self.handle_client, args=(client_socket,)) + client_handler.start() + + def handle_client(self, client_socket): + client_socket.sendall('220 Bienvenido al servidor FTP\r\n'.encode('utf-8')) + authenticated = False + username = '' + current_directory = self.working_directory + data_socket = None + + while True: + data = client_socket.recv(1024).decode('utf-8').strip() + if not data: + break + print(f"Comando recibido: {data}") + command, _, params = data.partition(' ') + command = command.upper() + + if command == 'USER': + username = params + client_socket.sendall('331 Nombre de usuario OK, necesita contraseña\r\n'.encode('utf-8')) + + elif command == 'PASS': + if username in self.users and self.users[username] == params: + authenticated = True + client_socket.sendall('230 Usuario autenticado exitosamente\r\n'.encode('utf-8')) + else: + client_socket.sendall('530 Error de autenticación\r\n'.encode('utf-8')) + + elif not authenticated: + client_socket.sendall('530 Debe autenticarse primero\r\n'.encode('utf-8')) + + elif command == 'SYST': + client_socket.sendall('215 UNIX Type: L8\r\n'.encode('utf-8')) + + elif command == 'PWD': + relative_path = os.path.relpath(current_directory, self.working_directory) + if relative_path == '.': + relative_path = '/' + else: + relative_path = '/' + relative_path.replace(os.sep, '/') + response = f'257 "{relative_path}"\r\n' + client_socket.sendall(response.encode('utf-8')) + + elif command == 'CWD': + new_dir = params + if new_dir.startswith('/'): + target_directory = os.path.join(self.working_directory, new_dir.strip('/')) + else: + target_directory = os.path.join(current_directory, new_dir) + if os.path.isdir(target_directory): + current_directory = os.path.abspath(target_directory) + client_socket.sendall('250 Directorio cambiado correctamente\r\n'.encode('utf-8')) + else: + client_socket.sendall('550 Directorio no encontrado\r\n'.encode('utf-8')) + + elif command == 'PASV': + if data_socket: + data_socket.close() + data_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + data_socket.bind((self.host, 0)) + data_socket.listen(1) + ip_address = self.get_ip_address() + port_number = data_socket.getsockname()[1] + p1 = port_number >> 8 + p2 = port_number & 0xFF + ip_parts = ip_address.split('.') + response = f'227 Entering Passive Mode ({",".join(ip_parts)},{p1},{p2})\r\n' + client_socket.sendall(response.encode('utf-8')) + + elif command == 'LIST': + if data_socket: + client_socket.sendall('150 Aquí viene la lista de directorios\r\n'.encode('utf-8')) + conn, _ = data_socket.accept() + files = os.listdir(current_directory) + listing = '' + for name in files: + path = os.path.join(current_directory, name) + if os.path.isdir(path): + listing += f"drwxr-xr-x 1 user group 0 Jan 1 00:00 {name}\r\n" + else: + size = os.path.getsize(path) + listing += f"-rw-r--r-- 1 user group {size:>6} Jan 1 00:00 {name}\r\n" + conn.sendall(listing.encode('utf-8')) + conn.close() + data_socket.close() + data_socket = None + client_socket.sendall('226 Listado enviado correctamente\r\n'.encode('utf-8')) + else: + client_socket.sendall('425 Use PASV primero\r\n'.encode('utf-8')) + + elif command == 'RETR': + if data_socket: + filepath = os.path.join(current_directory, params) + if os.path.isfile(filepath): + client_socket.sendall('150 Iniciando transferencia de datos\r\n'.encode('utf-8')) + conn, _ = data_socket.accept() + with open(filepath, 'rb') as f: + while True: + chunk = f.read(1024) + if not chunk: + break + conn.sendall(chunk) + conn.close() + data_socket.close() + data_socket = None + client_socket.sendall('226 Transferencia completada\r\n'.encode('utf-8')) + else: + client_socket.sendall('550 Archivo no encontrado\r\n'.encode('utf-8')) + else: + client_socket.sendall('425 Use PASV primero\r\n'.encode('utf-8')) + + elif command == 'STOR': + if data_socket: + filepath = os.path.join(current_directory, params) + client_socket.sendall('150 Iniciando transferencia de datos\r\n'.encode('utf-8')) + conn, _ = data_socket.accept() + with open(filepath, 'wb') as f: + while True: + chunk = conn.recv(1024) + if not chunk: + break + f.write(chunk) + conn.close() + data_socket.close() + data_socket = None + client_socket.sendall('226 Transferencia completada\r\n'.encode('utf-8')) + else: + client_socket.sendall('425 Use PASV primero\r\n'.encode('utf-8')) + + elif command == 'DELE': + filepath = os.path.join(current_directory, params) + if os.path.isfile(filepath): + os.remove(filepath) + client_socket.sendall('250 Archivo eliminado correctamente\r\n'.encode('utf-8')) + else: + client_socket.sendall('550 Archivo no encontrado\r\n'.encode('utf-8')) + + elif command == 'MKD': + dirpath = os.path.join(current_directory, params) + try: + os.makedirs(dirpath) + response = f'257 "{params}" creado\r\n' + client_socket.sendall(response.encode('utf-8')) + except OSError: + client_socket.sendall('550 No se pudo crear el directorio\r\n'.encode('utf-8')) + + elif command == 'RMD': + dirpath = os.path.join(current_directory, params) + try: + os.rmdir(dirpath) + client_socket.sendall('250 Directorio eliminado correctamente\r\n'.encode('utf-8')) + except OSError: + client_socket.sendall('550 No se pudo eliminar el directorio\r\n'.encode('utf-8')) + + elif command == 'RNFR': + filepath = os.path.join(current_directory, params) + if os.path.exists(filepath): + client_socket.sendall('350 Archivo/directorio existe, listo para RNTO\r\n'.encode('utf-8')) + data = client_socket.recv(1024).decode('utf-8').strip() + cmd, _, new_name = data.partition(' ') + if cmd.upper() == 'RNTO': + new_filepath = os.path.join(current_directory, new_name) + os.rename(filepath, new_filepath) + client_socket.sendall('250 Renombrado correctamente\r\n'.encode('utf-8')) + else: + client_socket.sendall('503 Se esperaba RNTO\r\n'.encode('utf-8')) + else: + client_socket.sendall('550 Archivo/directorio no encontrado\r\n'.encode('utf-8')) + + elif command == 'QUIT': + client_socket.sendall('221 Adiós\r\n'.encode('utf-8')) + break + + else: + client_socket.sendall('502 Comando no implementado\r\n'.encode('utf-8')) + + client_socket.close() + if data_socket: + data_socket.close() + print("Conexión cerrada") + + def get_ip_address(self): + # Obtener la dirección IP local + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + try: + # Conectar a un host remoto para obtener la IP local + s.connect(('8.8.8.8', 80)) + ip = s.getsockname()[0] + except Exception: + ip = '127.0.0.1' + finally: + s.close() + return ip + +def main(): + server = FTPServer() + server.start() + +if __name__ == '__main__': + main() diff --git a/run.sh b/run.sh index 475b295..c036f8f 100644 --- a/run.sh +++ b/run.sh @@ -1,5 +1,5 @@ -PROTOCOL=0 +#!/bin/bash # Replace the next shell command with the entrypoint of your solution -echo $@ \ No newline at end of file +python main/client.py "$@" \ No newline at end of file diff --git a/tests/ftp/dist/ftpserver b/tests/ftp/dist/ftpserver new file mode 100644 index 0000000..2c0150e Binary files /dev/null and b/tests/ftp/dist/ftpserver differ diff --git a/tests/ftp/files/2.txt b/tests/ftp/files/2.txt new file mode 100644 index 0000000..3ed2ffa --- /dev/null +++ b/tests/ftp/files/2.txt @@ -0,0 +1 @@ +this file is for test use only \ No newline at end of file diff --git a/tests/ftp/files/directory/1.txt b/tests/ftp/files/directory/1.txt new file mode 100644 index 0000000..687548b --- /dev/null +++ b/tests/ftp/files/directory/1.txt @@ -0,0 +1 @@ +this is some test data \ No newline at end of file diff --git a/tests/ftp/install.sh b/tests/ftp/install.sh index 6bc8f5c..7cbf0c8 100644 --- a/tests/ftp/install.sh +++ b/tests/ftp/install.sh @@ -1 +1,2 @@ -docker run --rm -d --name ftpd_server -p 21:21 -p 30000-30009:30000-30009 stilliard/pure-ftpd bash /run.sh -c 30 -C 10 -l puredb:/etc/pure-ftpd/pureftpd.pdb -E -j -R -P localhost -p 30000:30059 \ No newline at end of file +docker run -d --rm --name vsftpd -p 21:21 -p 21100-21110:21100-21110 -e "PASV_ADDRESS=127.0.0.1" -v $PWD/tests/ftp/files:/home/vsftpd/user lhauspie/vsftpd-alpine +echo "a new file for upload" >> tests/ftp/new.txt \ No newline at end of file diff --git a/tests/ftp/run.sh b/tests/ftp/run.sh index 091108f..2016fbe 100644 --- a/tests/ftp/run.sh +++ b/tests/ftp/run.sh @@ -1 +1,7 @@ -./run.sh --user test --body test \ No newline at end of file +#!/bin/bash + +python3 tests/ftp/tester.py + +if [[ $? -ne 0 ]]; then + exit 1 +fi \ No newline at end of file diff --git a/tests/ftp/tester.py b/tests/ftp/tester.py new file mode 100644 index 0000000..a45a860 --- /dev/null +++ b/tests/ftp/tester.py @@ -0,0 +1,52 @@ +import subprocess, sys + +def make_test(args, expeteted_output, error_msg): + command = f"./run.sh {args}" + + info = subprocess.run([x for x in command.split(' ')], capture_output=True, text=True) + output = info.stdout + err = info.stderr + + if len(output) > 0: + print(f"Execution output: {output}") + + if len(err) > 0: + print(f"An error ocurred during execution: {err}") + + + if not all([x in output for x in expeteted_output]): + print("\033[31m" + f"Test: {command} failed with error {error_msg}") + return False + + print("\033[32m" + f"Test: {command} completed") + + return True + + +# initial folder structure +# /: 1. directory 2. 2.txt +# /directory: 1.txt + +tests = [ + ("-h 127.0.0.1 -p 21 -u user -w pass", ("220","230",), "Login Failed"), + ("-h 127.0.0.1 -p 21 -u user -w pass -c PWD", ("257",), "/ directory listing failed"), + ("-h 127.0.0.1 -p 21 -u user -w pass -c CWD -a /directory", ("250",), "change directory failed"), + ("-h 127.0.0.1 -p 21 -u user -w pass -c QUIT", ("221",), "exiting ftp server failed"), + ("-h 127.0.0.1 -p 21 -u user -w pass -c RETR -a 2.txt" , ("150","226",), "could not retrieve 2.txt file"), + ("-h 127.0.0.1 -p 21 -u user -w pass -c STOR -a tests/ftp/new.txt -b new.txt", ("150", "226",), "file new.txt upload failed"), + ("-h 127.0.0.1 -p 21 -u user -w pass -c RNFR -a 2.txt -b 3.txt", ("350", "250",), "rename from 2.txt to 3.txt failed"), + ("-h 127.0.0.1 -p 21 -u user -w pass -c DELE -a new.txt", ("250",), "delete new.txt failed"), + ("-h 127.0.0.1 -p 21 -u user -w pass -c MKD -a directory2", ("257",), "directory directory2 creation failed"), + ("-h 127.0.0.1 -p 21 -u user -w pass -c RMD -a directory2", ("250",), "directory directory2 removal failed"), +] + +succeed = True + +for x in tests: + succeed = make_test(x[0],x[1],x[2]) and succeed + +if not succeed: + print("Errors ocurred during tests process") + sys.exit(1) + +print("All commands executed successfully") \ No newline at end of file diff --git a/tests/http/install.sh b/tests/http/install.sh index ca3818f..e69de29 100644 --- a/tests/http/install.sh +++ b/tests/http/install.sh @@ -1 +0,0 @@ -docker run -d -p 80:80 tiangolo/uvicorn-gunicorn-fastapi diff --git a/tests/http/run.sh b/tests/http/run.sh index 091108f..620852d 100644 --- a/tests/http/run.sh +++ b/tests/http/run.sh @@ -1 +1,18 @@ -./run.sh --user test --body test \ No newline at end of file +#!/bin/bash + +# Iniciar el servidor +echo "Iniciando el servidor..." +./tests/http/server & +SERVER_PID=$! + +# Esperar un poco para asegurarnos de que el servidor esté completamente iniciado +sleep 2 + +# Ejecutar las pruebas +echo "Ejecutando las pruebas..." +python3 ./tests/http/tests.py + +if [[ $? -ne 0 ]]; then + echo "HTTP test failed" + exit 1 +fi \ No newline at end of file diff --git a/tests/http/server b/tests/http/server new file mode 100755 index 0000000..25fb970 Binary files /dev/null and b/tests/http/server differ diff --git a/tests/http/tests.py b/tests/http/tests.py new file mode 100644 index 0000000..e1b40f4 --- /dev/null +++ b/tests/http/tests.py @@ -0,0 +1,134 @@ +import os, sys +import json + +def make_request(method, path, headers=None, data=None): + headerstr = "-h {}" if headers is None else f" -h {headers}" + datastr = "" if data is None else f" -d {data}" + response_string = os.popen(f"sh run.sh -m {method} -u http://localhost:8080{path} {headerstr} {datastr}").read() + return json.loads(response_string) # JSON con campos status, body y headers + +# Almacena los resultados de las pruebas +results = [] + +def print_case(case, description): + print(f"\n👉 \033[1mCase: {case}\033[0m") + print(f" 📝 {description}") + +def evaluate_response(case, expected_status, actual_status, expected_body=None, actual_body=None): + success = actual_status == expected_status and (expected_body is None or expected_body in actual_body) + results.append({ + "case": case, + "status": "Success" if success else "Failed", + "expected_status": expected_status, + "actual_status": actual_status, + "expected_body": expected_body, + "actual_body": actual_body + }) + if success: + print(f" ✅ \033[92mSuccess\033[0m") + else: + print(f" ❌ \033[91mFailed\033[0m") + +# Pruebas de casos simples +print_case("GET root", "Testing a simple GET request to '/' without authorization") +response = make_request("GET", "/") +evaluate_response("GET root", 200, response['status'], "Welcome to the server!", response['body']) + +print_case("POST simple body", "Testing POST request to '/' with a plain text body") +response = make_request("POST", "/", data="Hello, server!") +evaluate_response("POST simple body", 200, response['status'], "POST request successful", response['body']) + +print_case("HEAD root", "Testing a simple HEAD request to '/' without authorization") +response = make_request("HEAD", "/") +evaluate_response("HEAD root", 200, response['status']) + +# Pruebas de casos avanzados (con autorización) +print_case("GET secure without Authorization", "Testing GET request to '/secure' without authorization") +response = make_request("GET", "/secure") +evaluate_response("GET secure without Authorization", 401, response['status'], "Authorization header missing", response['body']) + +print_case("GET secure with valid Authorization", "Testing GET request to '/secure' with valid authorization") +response = make_request("GET", "/secure", headers='{\\"Authorization\\":\\"Bearer\\ 12345\\"}') +evaluate_response("GET secure with valid Authorization", 200, response['status'], "You accessed a protected resource", response['body']) + +print_case("GET secure with invalid Authorization", "Testing GET request to '/secure' with invalid authorization") +response = make_request("GET", "/secure", headers='{\\"Authorization\\":\\ \\"Bearer\\ invalid_token\\"}') +evaluate_response("GET secure with invalid Authorization", 401, response['status'], "Invalid or missing authorization token", response['body']) + +# Ajuste en PUT request +print_case("PUT request", "Testing a simple PUT request to '/resource'") +response = make_request("PUT", "/resource") +evaluate_response("PUT request", 200, response['status'], "PUT request successful! Resource '/resource' would be updated if this were implemented.", response['body']) + +# Ajuste en DELETE request +print_case("DELETE request", "Testing DELETE request to '/resource'") +response = make_request("DELETE", "/resource") +evaluate_response("DELETE request", 200, response['status'], "DELETE request successful! Resource '/resource' would be deleted if this were implemented.", response['body']) + +print_case("OPTIONS request", "Testing OPTIONS request to '/'") +response = make_request("OPTIONS", "/") +evaluate_response("OPTIONS request", 204, response['status']) + +print_case("TRACE request", "Testing TRACE request to '/'") +response = make_request("TRACE", "/") +evaluate_response("TRACE request", 200, response['status']) + +print_case("CONNECT request", "Testing CONNECT request to '/target'") +response = make_request("CONNECT", "/target") +evaluate_response("CONNECT request", 200, response['status'], "CONNECT method successful", response['body']) + +# Ajuste en Malformed POST body +print_case("Malformed POST body", "Testing POST request with malformed JSON body") +response = make_request( + "POST", + "/secure", + headers='{\\"Authorization\\":\\ \\"Bearer\\ 12345\\",\\ \\"Content-Type\\":\\ \\"application/json\\"}', + data='{"key":}' +) +evaluate_response( + "Malformed POST body", + 400, + response['status'], + "Malformed JSON body", + response['body'] +) + +# Nuevo caso: Malformed POST body without Authorization +print_case("Malformed POST body without Authorization", "Testing POST request with malformed JSON body and no authorization") +response = make_request( + "POST", + "/secure", + headers='{\\"Content-Type\\":\\ \\"application/json\\"}', + data='{"key":}' +) +evaluate_response( + "Malformed POST body without Authorization", + 401, + response['status'], + "Authorization header missing", + response['body'] +) + +print_case("Invalid Method", "Testing an unsupported method (PATCH)") +response = make_request("PATCH", "/") +evaluate_response("Invalid Method", 405, response['status']) + +# Resumen +print("\n🎉 \033[1mTest Summary\033[0m 🎉") +total_cases = len(results) +success_cases = sum(1 for result in results if result["status"] == "Success") +failed_cases = total_cases - success_cases + +print(f" ✅ Successful cases: {success_cases}/{total_cases}") + +if failed_cases > 0: + print(f" ❌ Failed cases: {failed_cases}/{total_cases}") + print("\n📋 \033[1mFailed Cases Details:\033[0m") + for result in results: + if result["status"] == "Failed": + print(f" ❌ {result['case']}") + print(f" - Expected status: {result['expected_status']}, Actual status: {result['actual_status']}") + if result['expected_body'] and result['actual_body']: + print(f" - Expected body: {result['expected_body']}") + print(f" - Actual body: {result['actual_body']}\n") + sys.exit(1) diff --git a/tests/irc/dist/server b/tests/irc/dist/server new file mode 100644 index 0000000..938cedf Binary files /dev/null and b/tests/irc/dist/server differ diff --git a/tests/irc/exec.sh b/tests/irc/exec.sh new file mode 100644 index 0000000..7ba9744 --- /dev/null +++ b/tests/irc/exec.sh @@ -0,0 +1,84 @@ +#!/bin/bash + +# Función para mostrar ayuda +function show_help() { + echo "Uso: ./exec.sh -H -p -n -c -a " +} + +# Variables por defecto +host="localhost" +port="8080" +nick="NICK" # Valor por defecto para el nick +command="" +argument="" + +# Procesar argumentos +while getopts "H:p:n:c:a:" opt; do + case $opt in + H) host="$OPTARG" ;; + p) port="$OPTARG" ;; + n) nick="$OPTARG" ;; # Capturar el valor de -n + c) command="$OPTARG" ;; + a) argument="$OPTARG" ;; + *) show_help + exit 1 ;; + esac +done + +# Verificar que los argumentos requeridos estén presentes +if [ -z "$command" ] || [ -z "$argument" ]; then + show_help + exit 1 +fi + +# Ejecutar el cliente IRC con los parámetros +output=$(./run.sh -H "$host" -p "$port" -n "$nick" -c "$command" -a "$argument") + +# Verificar la salida del cliente según el comando enviado +case "$command" in + "/nick") + expected_response="Tu nuevo apodo es $argument" + ;; + "/join") + expected_response="Te has unido al canal $argument" + ;; + "/part") + expected_response="Has salido del canal $argument" + ;; + "/privmsg") + expected_response="Mensaje de $nick: $argument" + ;; + "/notice") + expected_response="Notificacion de $nick: $argument" + ;; + "/list") + expected_response="Lista de canales:" + ;; + "/names") + expected_response="Usuarios en el canal $argument:" + ;; + "/whois") + expected_response="Usuario $argument en el canal" + ;; + "/topic") + expected_response="El topic del canal $argument es:" + ;; + "/quit") + expected_response="Desconectado del servidor" + ;; + *) + echo "Comando no reconocido: $command" + exit 1 + ;; +esac + +# Verificar si la salida del cliente coincide con lo esperado +if [[ "$output" == *"$expected_response"* ]]; then + echo -e "\e[32mPrueba exitosa: La salida del cliente coincide con lo esperado.\e[0m" + exit 0 +else + echo -e "\e[31mPrueba fallida: La salida del cliente no coincide con lo esperado.\e[0m" + echo -e "\e[31mEsperado: $expected_response\e[0m" + echo -e "\e[31mObtenido: $output\e[0m" + exit 1 +fi \ No newline at end of file diff --git a/tests/irc/install.sh b/tests/irc/install.sh index 2afc777..6d9ab7a 100644 --- a/tests/irc/install.sh +++ b/tests/irc/install.sh @@ -1 +1,2 @@ -docker run -d ircd/unrealircd:edge +echo "Executing server" +python3 "tests/irc/tester.py" \ No newline at end of file diff --git a/tests/irc/run.sh b/tests/irc/run.sh index 091108f..6d3e7db 100644 --- a/tests/irc/run.sh +++ b/tests/irc/run.sh @@ -1 +1,50 @@ -./run.sh --user test --body test \ No newline at end of file +#!/bin/bash + +failed=0 + +# Test 1: Conectar, establecer y cambiar nickname +echo "Running Test 1: Conectar, establecer y cambiar nickname" +./tests/irc/exec.sh -H "localhost" -p "8080" -n "TestUser1" -c "/nick" -a "NuevoNick" +if [[ $? -ne 0 ]]; then + echo "Test 1 failed" + failed=1 +fi + +# Test 2: Entrar a un canal +echo "Running Test 2: Entrar a un canal" +./tests/irc/exec.sh -H "localhost" -p "8080" -n "TestUser1" -c "/join" -a "#Nuevo" +if [[ $? -ne 0 ]]; then + echo "Test 2 failed" + failed=1 +fi + +# Test 3: Enviar un mensaje a un canal +echo "Running Test 3: Enviar un mensaje a un canal" +./tests/irc/exec.sh -H "localhost" -p "8080" -n "TestUser1" -c "/notice" -a "#General Hello, world!" +if [[ $? -ne 0 ]]; then + echo "Test 3 failed" + failed=1 +fi + +# Test 4: Salir de un canal +echo "Running Test 4: Salir de un canal" +./tests/irc/exec.sh -H "localhost" -p "8080" -n "NewNick" -c "/part" -a "#General" +if [[ $? -ne 0 ]]; then + echo "Test 5 failed" + failed=1 +fi + +# Test 5: Desconectar del servidor +echo "Running Test 5: Desconectar del servidor" +./tests/irc/exec.sh -H "localhost" -p "8080" -n "NewNick" -c "/quit" -a "Goodbye!" +if [[ $? -ne 0 ]]; then + echo "Test 6 failed" + failed=1 +fi + +if [[ $failed -ne 0 ]]; then + echo "Tests failed" + exit 1 +fi + +echo "All custom tests completed successfully" \ No newline at end of file diff --git a/tests/irc/tester.py b/tests/irc/tester.py new file mode 100644 index 0000000..b109722 --- /dev/null +++ b/tests/irc/tester.py @@ -0,0 +1,9 @@ +import subprocess + +# Path to the server executable +server_executable_path = 'tests/irc/dist/server' + +# Run the server executable in the background +server_process = subprocess.Popen([server_executable_path], stdout=subprocess.PIPE, stderr=subprocess.PIPE) + +print("Server executed successfully") \ No newline at end of file diff --git a/tests/smtp/install.sh b/tests/smtp/install.sh index c9c802b..e69de29 100644 --- a/tests/smtp/install.sh +++ b/tests/smtp/install.sh @@ -1 +0,0 @@ -docker run --rm -d -p 5000:80 -p 2525:25 rnwood/smtp4dev \ No newline at end of file diff --git a/tests/smtp/run.sh b/tests/smtp/run.sh index 091108f..b19d796 100644 --- a/tests/smtp/run.sh +++ b/tests/smtp/run.sh @@ -1 +1,18 @@ -./run.sh --user test --body test \ No newline at end of file +#!/bin/bash + +# Iniciar el servidor +echo "Iniciando el servidor..." +./tests/smtp/server & +SERVER_PID=$! + +# Esperar un poco para asegurarnos de que el servidor esté completamente iniciado +sleep 2 + +# Ejecutar las pruebas +echo "Ejecutando las pruebas..." +python3 ./tests/smtp/tests.py + +if [[ $? -ne 0 ]]; then + echo "SMTP test failed" + exit 1 +fi \ No newline at end of file diff --git a/tests/smtp/server b/tests/smtp/server new file mode 100755 index 0000000..99d305e Binary files /dev/null and b/tests/smtp/server differ diff --git a/tests/smtp/tests.py b/tests/smtp/tests.py new file mode 100644 index 0000000..6545797 --- /dev/null +++ b/tests/smtp/tests.py @@ -0,0 +1,137 @@ +import os, sys +import json + +def send_email(from_address, to_addresses, subject, body, headers=None): + headerstr = "-h {}" if headers is None else f" -h {headers}" + response_string = os.popen(f"sh run.sh -u localhost -p 2525 -f {from_address} -t {to_addresses} -s {subject} {headerstr} -b {body}").read() + return json.loads(response_string) + +# Almacena los resultados de las pruebas +results = [] + +def print_case(case, description): + print(f"\n👉 \033[1mCase: {case}\033[0m") + print(f" 📝 {description}") + +def evaluate_response(case, expected_status, actual_status, expected_message=None, actual_message=None): + success = f'{actual_status}' == f'{expected_status}' and (expected_message is None or expected_message in actual_message) + results.append({ + "case": case, + "status": "Success" if success else "Failed", + "expected_status": expected_status, + "actual_status": actual_status, + "expected_message": expected_message, + "actual_message": actual_message + }) + if success: + print(f" ✅ \033[92mSuccess\033[0m") + else: + print(f" ❌ \033[91mFailed\033[0m") + +# Caso 1: Envío de correo simple +print_case("Send simple email", "Enviar un correo simple sin encabezados adicionales") +response = send_email( + from_address="sender@example.com", + to_addresses='[\\"recipient@example.com\\"]', + subject="Simple Email", + body="This is a simple email." +) +evaluate_response("Send simple email", 250, response["status_code"], "Message accepted for delivery", response["message"]) + +# Caso 2: Envío de correo con encabezados adicionales +print_case("Send email with CC", "Enviar un correo con encabezados adicionales (CC)") +response = send_email( + from_address="sender@example.com", + to_addresses='[\\"recipient@example.com\\"]', + subject="Email with CC", + body="This email includes a CC header.", + headers='{\\"CC\\":\\ \\"cc@example.com\\"}' +) +evaluate_response("Send email with CC", 250, response["status_code"], "Message accepted for delivery", response["message"]) + +# Caso 3: Envío de correo con múltiples destinatarios +print_case("Send email to multiple recipients", "Enviar un correo a múltiples destinatarios") +response = send_email( + from_address="sender@example.com", + to_addresses='[\\"recipient1@example.com\\",\\ \\"recipient2@example.com\\"]', + subject="Multiple Recipients", + body="This email is sent to multiple recipients." +) +evaluate_response("Send email to multiple recipients", 250, response["status_code"], "Message accepted for delivery", response["message"]) + +# Caso 4: Envío de correo con mensaje mal formado +print_case("Malformed email body", "Enviar un correo con un cuerpo mal formado") +response = send_email( + from_address="sender@example.com", + to_addresses='[\\"recipient@example.com\\"]', + subject="Malformed Body", + body=None # Este caso puede simular un cuerpo mal formado +) +evaluate_response("Malformed email body", 250, response["status_code"], "Message accepted for delivery", response["message"]) + +# Caso 5: Envío con encabezados vacíos +print_case("Send email with empty headers", "Enviar un correo con encabezados vacíos") +response = send_email( + from_address="sender@example.com", + to_addresses='[\\"recipient@example.com\\"]', + subject="Empty Headers", + body="This email has empty headers.", + headers='{}' +) +evaluate_response("Send email with empty headers", 250, response["status_code"], "Message accepted for delivery", response["message"]) + +print_case("Send email without 'From' address", "Enviar un correo sin la dirección 'From'") +response = send_email( + from_address=None, # Sin dirección 'From' + to_addresses='[\\"recipient@example.com\\"]', + subject="No From Address", + body="This email has no 'From' address." +) +evaluate_response("Send email without 'From' address", 501, response["status_code"], "Invalid sender address", response["message"]) + +print_case("Send email with invalid recipient address", "Enviar un correo con una dirección de destinatario inválida") +response = send_email( + from_address="sender@example.com", + to_addresses='[\\"invalidemail@com\\"]', # Dirección inválida + subject="Invalid Recipient", + body="This email has an invalid recipient address." +) +evaluate_response("Send email with invalid recipient address", 550, response["status_code"], "Invalid recipient address", response["message"]) + +print_case("Send email with empty body", "Enviar un correo con un cuerpo vacío") +response = send_email( + from_address="sender@example.com", + to_addresses='[\\"recipient@example.com\\"]', + subject="Empty Body", + body="" # Cuerpo vacío +) +evaluate_response("Send email with empty body", 250, response["status_code"], "Message accepted for delivery", response["message"]) + +print_case("Send email with empty subject", "Enviar un correo con un asunto vacío") +response = send_email( + from_address="sender@example.com", + to_addresses='[\\"recipient@example.com\\"]', + subject="", # Asunto vacío + body="This email has no subject." +) +evaluate_response("Send email with empty subject", 250, response["status_code"], "Message accepted for delivery", response["message"]) + +# Resumen de los resultados +print("\n🎉 \033[1mTest Summary\033[0m 🎉") +total_cases = len(results) +success_cases = sum(1 for result in results if result["status"] == "Success") +failed_cases = total_cases - success_cases + +print(f" ✅ Successful cases: {success_cases}/{total_cases}") + +if failed_cases > 0: + print(f" ❌ Failed cases: {failed_cases}/{total_cases}") + print("\n📋 \033[1mFailed Cases Details:\033[0m") + for result in results: + if result["status"] == "Failed": + print(f" ❌ {result['case']}") + print(f" - Expected status: {result['expected_status']}, Actual status: {result['actual_status']}") + if result['expected_message'] and result['actual_message']: + print(f" - Expected message: {result['expected_message']}") + print(f" - Actual message: {result['actual_message']}\n") + sys.exit(1)