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
+
+
+ 1739318957045
+
+
+
+
+
+
+
+
+
+
+
+ 1739320131063
+
+
+
+ 1739320131063
+
+
+
+ 1739331831190
+
+
+
+ 1739331831190
+
+
+
+ 1739334182801
+
+
+
+ 1739334182801
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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)