diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..3a59887 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,2 @@ +# CODEOWNERS +/FTP/ @Javi111003 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..66b44de --- /dev/null +++ b/.gitignore @@ -0,0 +1,42 @@ +# Ignorar PyCharm +.idea/ + +# Archivos de compilación de Python +__pycache__/ +*.py[cod] +*$py.class + +# Entorno virtual +venv/ + +# Archivos de logs +*.log + +# Archivos temporales +*.tmp +*.swp +*.bak +*.old + +# Configuración local de editores +*.vscode/ + +# Pruebas y cobertura +htmlcov/ +*.cover +.coverage +*.coverage.* +.cache +nosetests.xml +coverage.xml + +# Bases de datos y archivos grandes +*.sqlite3 +*.csv + +# Variables de entorno +.env + +# Carpeta de descargas FTP +Downloads/ +FTP/Downloads/ diff --git a/FTP/client.py b/FTP/client.py new file mode 100644 index 0000000..f0b15ce --- /dev/null +++ b/FTP/client.py @@ -0,0 +1,174 @@ +import socket +import os +from pathlib import Path + +class FTPClient: + def __init__(self, host='127.0.0.1', port=21): + self.host = host + self.port = port + self.commands = {} + self._register_commands() + self.downloads_folder = str(Path.cwd() / "Downloads") # Carpeta local Downloads + # Crear la carpeta si no existe + os.makedirs(self.downloads_folder, exist_ok=True) + print(f"Carpeta de descargas: {self.downloads_folder}") + + def _register_commands(self): + # Registro de comandos con sus descripciones + self.add_command("USER", "Especifica el usuario") + self.add_command("PASS", "Especifica la contraseña") + self.add_command("PWD", "Muestra el directorio actual") + self.add_command("CWD", "Cambia el directorio de trabajo") + self.add_command("LIST", "Lista archivos y directorios") + self.add_command("MKD", "Crea un directorio") + self.add_command("RMD", "Elimina un directorio") + self.add_command("DELE", "Elimina un archivo") + self.add_command("RNFR", "Especifica el archivo a renombrar") + self.add_command("RNTO", "Especifica el nuevo nombre") + self.add_command("QUIT", "Cierra la conexión") + self.add_command("HELP", "Muestra la ayuda") + self.add_command("SYST", "Muestra información del sistema") + self.add_command("NOOP", "No realiza ninguna operación") + self.add_command("ACCT", "Especifica la cuenta del usuario") + self.add_command("SMNT", "Monta una estructura de sistema de archivos") + self.add_command("REIN", "Reinicia la conexión") + self.add_command("PORT", "Especifica dirección y puerto para conexión") + self.add_command("PASV", "Entra en modo pasivo") + self.add_command("TYPE", "Establece el tipo de transferencia") + self.add_command("STRU", "Establece la estructura de archivo") + self.add_command("MODE", "Establece el modo de transferencia") + self.add_command("RETR", "Recupera un archivo") + self.add_command("STOR", "Almacena un archivo") + self.add_command("STOU", "Almacena un archivo con nombre único") + self.add_command("APPE", "Añade datos a un archivo") + self.add_command("ALLO", "Reserva espacio") + self.add_command("REST", "Reinicia transferencia desde punto") + self.add_command("ABOR", "Aborta operación en progreso") + self.add_command("SITE", "Comandos específicos del sitio") + self.add_command("STAT", "Retorna estado actual") + self.add_command("NLST", "Lista nombres de archivos") + + def add_command(self, cmd_name, description): + """Añade un nuevo comando al cliente""" + self.commands[cmd_name] = description + + def send_command(self, sock, command, *args): + full_command = f"{command} {' '.join(args)}".strip() + sock.send(f"{full_command}\r\n".encode()) + return sock.recv(1024).decode() + + def send_file(self, sock, filename): + """Envía un archivo al servidor""" + try: + with open(filename, 'rb') as f: + data = f.read() + sock.send(data) + return True + except: + return False + + def receive_file(self, sock, filename): + """Recibe un archivo del servidor en la carpeta Downloads local""" + try: + # Construir la ruta completa en la carpeta Downloads local + download_path = os.path.join(self.downloads_folder, filename) + with open(download_path, 'wb') as f: + while True: + data = sock.recv(1024) + if not data or b"226" in data: # Detectar fin de transferencia + break + f.write(data) + print(f"Archivo guardado en: {download_path}") + return True + except Exception as e: + print(f"Error al recibir archivo: {e}") + return False + + def start(self): + client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + try: + client_socket.connect((self.host, self.port)) + print(client_socket.recv(1024).decode()) + + while True: + try: + command = input("FTP> ").strip().split() + if not command: + continue + + cmd = command[0].upper() + args = command[1:] if len(command) > 1 else [] + + if cmd == "HELP": + if args: + cmd_help = args[0].upper() + if cmd_help in self.commands: + print(f"{cmd_help}: {self.commands[cmd_help]}") + else: + print(f"Comando '{cmd_help}' no reconocido") + else: + print("\nComandos disponibles:") + for cmd_name, desc in sorted(self.commands.items()): + print(f"{cmd_name}: {desc}") + continue + + if cmd in self.commands: + # Manejo especial para comandos de transferencia de archivos + if cmd in ["STOR", "RETR", "APPE"]: + if len(args) < 1: + print(f"Uso: {cmd} ") + continue + + filename = args[0] + if cmd == "STOR": + if os.path.exists(filename): + response = self.send_command(client_socket, cmd, filename) + print(response) + if "150" in response: + if self.send_file(client_socket, filename): + print("Archivo enviado exitosamente") + else: + print("Error al enviar archivo") + else: + print("Archivo no encontrado") + + elif cmd == "RETR": + response = self.send_command(client_socket, cmd, filename) + print(response) + if "150" in response: + if self.receive_file(client_socket, filename): + print("Archivo recibido exitosamente") + else: + print("Error al recibir archivo") + + elif cmd == "APPE": + if os.path.exists(filename): + response = self.send_command(client_socket, cmd, filename) + print(response) + if "150" in response: + if self.send_file(client_socket, filename): + print("Archivo anexado exitosamente") + else: + print("Error al anexar archivo") + else: + print("Archivo no encontrado") + + else: + response = self.send_command(client_socket, cmd, *args) + print(response) + if cmd == "QUIT": + break + else: + print("Comando no reconocido") + + except Exception as e: + print(f"Error: {e}") + + except Exception as e: + print(f"Error de conexión: {e}") + finally: + client_socket.close() + +if __name__ == "__main__": + client = FTPClient() + client.start() \ No newline at end of file diff --git a/FTP/server.py b/FTP/server.py new file mode 100644 index 0000000..9c70bbd --- /dev/null +++ b/FTP/server.py @@ -0,0 +1,377 @@ +import socket +import os +from pathlib import Path +import shutil + +class FTPServer: + def __init__(self, host='0.0.0.0', port=21): + self.host = host + self.port = port + self.current_user = None + self.base_dir = Path.cwd() + self.current_dir = self.base_dir + self.commands = {} + self._register_commands() + self.data_port = 20 + self.transfer_type = 'A' # ASCII por defecto + self.structure = 'F' # File por defecto + self.mode = 'S' # Stream por defecto + self.data_socket = None + + def _register_commands(self): + # Registro de comandos con sus funciones correspondientes + self.add_command("USER", self.handle_user) + self.add_command("PASS", self.handle_pass) + self.add_command("PWD", self.handle_pwd) + self.add_command("CWD", self.handle_cwd) + self.add_command("LIST", self.handle_list) + self.add_command("QUIT", self.handle_quit) + self.add_command("MKD", self.handle_mkd) + self.add_command("RMD", self.handle_rmd) + self.add_command("DELE", self.handle_dele) + self.add_command("RNFR", self.handle_rnfr) + self.add_command("RNTO", self.handle_rnto) + self.add_command("SYST", self.handle_syst) + self.add_command("HELP", self.handle_help) + self.add_command("NOOP", self.handle_noop) + self.add_command("ACCT", self.handle_acct) + self.add_command("SMNT", self.handle_smnt) + self.add_command("REIN", self.handle_rein) + self.add_command("PORT", self.handle_port) + self.add_command("PASV", self.handle_pasv) + self.add_command("TYPE", self.handle_type) + self.add_command("STRU", self.handle_stru) + self.add_command("MODE", self.handle_mode) + self.add_command("RETR", self.handle_retr) + self.add_command("STOR", self.handle_stor) + self.add_command("STOU", self.handle_stou) + self.add_command("APPE", self.handle_appe) + self.add_command("ALLO", self.handle_allo) + self.add_command("REST", self.handle_rest) + self.add_command("ABOR", self.handle_abor) + self.add_command("SITE", self.handle_site) + self.add_command("STAT", self.handle_stat) + self.add_command("NLST", self.handle_nlst) + self.add_command("CDUP", self.handle_cdup) + + def add_command(self, cmd_name, cmd_func): + """Añade un nuevo comando al servidor""" + self.commands[cmd_name] = cmd_func + + def start(self): + server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + server_socket.bind((self.host, self.port)) + server_socket.listen(5) + print(f"Servidor FTP iniciado en {self.host}:{self.port}") + + while True: + client_socket, client_address = server_socket.accept() + print(f"Cliente conectado: {client_address}") + self.handle_client(client_socket) + + def handle_client(self, client_socket): + client_socket.send(b"220 Bienvenido al servidor FTP\r\n") + self.rename_from = None # Para el comando RNFR/RNTO + + while True: + try: + data = client_socket.recv(1024).decode().strip() + if not data: + break + + print(f"Comando recibido: {data}") + cmd_parts = data.split() + cmd = cmd_parts[0].upper() + args = cmd_parts[1:] if len(cmd_parts) > 1 else [] + + if cmd in self.commands: + self.commands[cmd](client_socket, args) + else: + client_socket.send(b"502 Comando no implementado\r\n") + + except Exception as e: + print(f"Error: {e}") + break + + client_socket.close() + + # Implementación de comandos + def handle_user(self, client_socket, args): + self.current_user = args[0] if args else None + client_socket.send(b"331 Usuario OK, esperando contrasena\r\n") + + def handle_pass(self, client_socket, args): + if self.current_user: + client_socket.send(b"230 Usuario logueado exitosamente\r\n") + else: + client_socket.send(b"503 Primero ingrese el usuario\r\n") + + def handle_pwd(self, client_socket, args): + response = f"257 \"{self.current_dir.relative_to(self.base_dir)}\"\r\n" + client_socket.send(response.encode()) + + def handle_cwd(self, client_socket, args): + if not args: + client_socket.send(b"501 Sintaxis invalida\r\n") + return + try: + new_path = (self.current_dir / args[0]).resolve() + if new_path.exists() and new_path.is_dir(): + self.current_dir = new_path + client_socket.send(b"250 Directorio cambiado exitosamente\r\n") + else: + client_socket.send(b"550 Directorio no existe\r\n") + except: + client_socket.send(b"550 Error al cambiar directorio\r\n") + + def handle_list(self, client_socket, args): + try: + files = "\r\n".join(str(f.name) for f in self.current_dir.iterdir()) + response = f"150 Lista de archivos:\r\n{files}\r\n226 Transferencia completa\r\n" + client_socket.send(response.encode()) + except: + client_socket.send(b"550 Error al listar archivos\r\n") + + def handle_mkd(self, client_socket, args): + if not args: + client_socket.send(b"501 Sintaxis invalida\r\n") + return + try: + new_dir = (self.current_dir / args[0]) + new_dir.mkdir(parents=True, exist_ok=True) + client_socket.send(f"257 \"{new_dir}\" creado\r\n".encode()) + except: + client_socket.send(b"550 Error al crear directorio\r\n") + + def handle_rmd(self, client_socket, args): + if not args: + client_socket.send(b"501 Sintaxis invalida\r\n") + return + try: + dir_to_remove = (self.current_dir / args[0]) + if dir_to_remove.is_dir(): + shutil.rmtree(dir_to_remove) + client_socket.send(b"250 Directorio eliminado\r\n") + else: + client_socket.send(b"550 No es un directorio\r\n") + except: + client_socket.send(b"550 Error al eliminar directorio\r\n") + + def handle_dele(self, client_socket, args): + if not args: + client_socket.send(b"501 Sintaxis invalida\r\n") + return + try: + file_to_delete = (self.current_dir / args[0]) + if file_to_delete.is_file(): + file_to_delete.unlink() + client_socket.send(b"250 Archivo eliminado\r\n") + else: + client_socket.send(b"550 No es un archivo\r\n") + except: + client_socket.send(b"550 Error al eliminar archivo\r\n") + + def handle_rnfr(self, client_socket, args): + if not args: + client_socket.send(b"501 Sintaxis invalida\r\n") + return + file_path = (self.current_dir / args[0]) + if file_path.exists(): + self.rename_from = file_path + client_socket.send(b"350 Listo para RNTO\r\n") + else: + client_socket.send(b"550 Archivo no existe\r\n") + + def handle_rnto(self, client_socket, args): + if not self.rename_from or not args: + client_socket.send(b"503 Comando RNFR requerido primero\r\n") + return + try: + new_path = (self.current_dir / args[0]) + self.rename_from.rename(new_path) + client_socket.send(b"250 Archivo renombrado exitosamente\r\n") + except: + client_socket.send(b"553 Error al renombrar\r\n") + finally: + self.rename_from = None + + def handle_syst(self, client_socket, args): + client_socket.send(b"215 UNIX Type: L8\r\n") + + def handle_help(self, client_socket, args): + commands = ", ".join(sorted(self.commands.keys())) + response = f"214-Los siguientes comandos están disponibles:\r\n{commands}\r\n214 Fin de ayuda.\r\n" + client_socket.send(response.encode()) + + def handle_noop(self, client_socket, args): + client_socket.send(b"200 OK\r\n") + + def handle_quit(self, client_socket, args): + client_socket.send(b"221 Goodbye\r\n") + return True + + # Nuevos manejadores de comandos + def handle_acct(self, client_socket, args): + client_socket.send(b"230 No se requiere cuenta para este servidor\r\n") + + def handle_smnt(self, client_socket, args): + client_socket.send(b"502 SMNT no implementado\r\n") + + def handle_rein(self, client_socket, args): + self.current_user = None + self.current_dir = self.base_dir + client_socket.send(b"220 Servicio reiniciado\r\n") + + def handle_port(self, client_socket, args): + client_socket.send(b"200 Comando PORT no implementado\r\n") + + def handle_pasv(self, client_socket, args): + client_socket.send(b"502 Modo pasivo no implementado\r\n") + + def handle_type(self, client_socket, args): + if not args: + client_socket.send(b"501 Sintaxis: TYPE {A,E,I,L}\r\n") + return + type_code = args[0].upper() + if type_code in ['A', 'E', 'I', 'L']: + client_socket.send(f"200 Tipo establecido a {type_code}\r\n".encode()) + else: + client_socket.send(b"504 Tipo no soportado\r\n") + + def handle_stru(self, client_socket, args): + if not args: + client_socket.send(b"501 Sintaxis: STRU {F,R,P}\r\n") + return + stru_code = args[0].upper() + if stru_code == 'F': + client_socket.send(b"200 Estructura establecida a File\r\n") + else: + client_socket.send(b"504 Estructura no soportada\r\n") + + def handle_mode(self, client_socket, args): + if not args: + client_socket.send(b"501 Sintaxis: MODE {S,B,C}\r\n") + return + mode_code = args[0].upper() + if mode_code == 'S': + client_socket.send(b"200 Modo establecido a Stream\r\n") + else: + client_socket.send(b"504 Modo no soportado\r\n") + + def handle_retr(self, client_socket, args): + if not args: + client_socket.send(b"501 Sintaxis: RETR filename\r\n") + return + try: + file_path = self.current_dir / args[0] + if file_path.is_file(): + with open(file_path, 'rb') as f: + data = f.read() + client_socket.send(b"150 Iniciando transferencia\r\n") + client_socket.send(data) + client_socket.send(b"226 Transferencia completa\r\n") + else: + client_socket.send(b"550 Archivo no encontrado\r\n") + except: + client_socket.send(b"550 Error al leer archivo\r\n") + + def handle_stor(self, client_socket, args): + if not args: + client_socket.send(b"501 Sintaxis: STOR filename\r\n") + return + try: + file_path = self.current_dir / args[0] + client_socket.send(b"150 Listo para recibir datos\r\n") + + # Recibir datos + with open(file_path, 'wb') as f: + while True: + data = client_socket.recv(1024) + if not data: + break + f.write(data) + + client_socket.send(b"226 Transferencia completa\r\n") + except: + client_socket.send(b"550 Error al almacenar archivo\r\n") + + def handle_stou(self, client_socket, args): + try: + import tempfile + temp_file = tempfile.NamedTemporaryFile(delete=False, dir=self.current_dir) + temp_name = Path(temp_file.name).name + client_socket.send(f"150 Archivo será almacenado como {temp_name}\r\n".encode()) + + # Recibir datos + while True: + data = client_socket.recv(1024) + if not data: + break + temp_file.write(data) + + temp_file.close() + client_socket.send(f"226 Transferencia completa. Archivo guardado como {temp_name}\r\n".encode()) + except: + client_socket.send(b"550 Error al almacenar archivo\r\n") + + def handle_appe(self, client_socket, args): + if not args: + client_socket.send(b"501 Sintaxis: APPE filename\r\n") + return + try: + file_path = self.current_dir / args[0] + mode = 'ab' if file_path.exists() else 'wb' + + client_socket.send(b"150 Listo para recibir datos\r\n") + with open(file_path, mode) as f: + while True: + data = client_socket.recv(1024) + if not data: + break + f.write(data) + + client_socket.send(b"226 Transferencia completa\r\n") + except: + client_socket.send(b"550 Error al anexar al archivo\r\n") + + def handle_allo(self, client_socket, args): + client_socket.send(b"200 ALLO no necesario\r\n") + + def handle_rest(self, client_socket, args): + client_socket.send(b"502 REST no implementado\r\n") + + def handle_abor(self, client_socket, args): + client_socket.send(b"226 ABOR procesado\r\n") + + def handle_site(self, client_socket, args): + client_socket.send(b"200 Comando SITE no soportado\r\n") + + def handle_stat(self, client_socket, args): + response = "211-Estado del servidor FTP\r\n" + response += f" Usuario: {self.current_user}\r\n" + response += f" Directorio actual: {self.current_dir}\r\n" + response += "211 Fin del estado\r\n" + client_socket.send(response.encode()) + + def handle_nlst(self, client_socket, args): + try: + files = "\r\n".join(str(f.name) for f in self.current_dir.iterdir() if f.is_file()) + response = f"150 Lista de archivos:\r\n{files}\r\n226 Transferencia completa\r\n" + client_socket.send(response.encode()) + except: + client_socket.send(b"550 Error al listar archivos\r\n") + + def handle_cdup(self, client_socket, args): + try: + new_path = self.current_dir.parent + if new_path.exists(): + self.current_dir = new_path + client_socket.send(b"200 Directorio cambiado al padre\r\n") + else: + client_socket.send(b"550 No se puede subir mas\r\n") + except: + client_socket.send(b"550 Error al cambiar directorio\r\n") + +if __name__ == "__main__": + server = FTPServer() + server.start() \ No newline at end of file diff --git a/FTP/usual_commands.txt b/FTP/usual_commands.txt new file mode 100644 index 0000000..6a20037 --- /dev/null +++ b/FTP/usual_commands.txt @@ -0,0 +1,187 @@ +USER: + +El campo de argumento es una cadena Telnet que identifica al usuario. La identificación del usuario es la que requiere el servidor para acceder a su sistema de archivos. Este comando será normalmente el primer comando transmitido por el usuario después de que se establezcan las conexiones de control (algunos servidores pueden requerir esto). Información adicional de identificación en forma de contraseña y/o un comando de cuenta también puede ser requerida por algunos servidores. Los servidores pueden permitir que se ingrese un nuevo comando USER en cualquier momento para cambiar el control de acceso y/o la información de contabilidad. Esto tiene el efecto de eliminar cualquier información de usuario, contraseña y cuenta ya proporcionada y comenzar de nuevo la secuencia de inicio de sesión. Todos los parámetros de transferencia permanecen sin cambios y cualquier transferencia de archivo en progreso se completa bajo los parámetros de control de acceso antiguos. + +PASS: + +El campo de argumento es una cadena Telnet que especifica la contraseña del usuario. Este comando debe ser precedido inmediatamente por el comando de nombre de usuario y, para algunos sitios, completa la identificación del usuario para el control de acceso. Dado que la información de la contraseña es bastante sensible, en general es deseable "enmascararla" o suprimir su visualización. Parece que el servidor no tiene una manera infalible de lograr esto. Por lo tanto, es responsabilidad del proceso de usuario-FTP ocultar la información sensible de la contraseña. + + +ACCT: + +El campo de argumento es una cadena Telnet que identifica la cuenta del usuario. El comando no está necesariamente relacionado con el comando USER, ya que algunos sitios pueden requerir una cuenta para iniciar sesión y otros solo para un acceso específico, como almacenar archivos. En el último caso, el comando puede llegar en cualquier momento. +Hay códigos de respuesta para diferenciar estos casos para la automatización: cuando se requiere información de cuenta para iniciar sesión, la respuesta a un comando PASSword exitoso es el código de respuesta 332. Por otro lado, si no se requiere información de cuenta para iniciar sesión, la respuesta a un comando PASSword exitoso es 230; y si se necesita información de cuenta para un comando emitido más tarde en el diálogo, el servidor debe devolver una respuesta 332 o 532 dependiendo de si almacena (a la espera de recibir el comando ACCounT) o descarta el comando, respectivamente. + + +CWD: + +Este comando permite al usuario trabajar con un directorio o conjunto de datos diferente para el almacenamiento o la recuperación de archivos sin alterar su información de inicio de sesión o contabilidad. Los parámetros de transferencia también permanecen sin cambios. El argumento es una ruta que especifica un directorio o un designador de grupo de archivos dependiente del sistema. + + +CDUP: + +Este comando es un caso especial de CWD y se incluye para simplificar la implementación de programas para transferir árboles de directorios entre sistemas operativos que tienen diferentes sintaxis para nombrar el directorio principal. Los códigos de respuesta serán idénticos a los códigos de respuesta de CWD. + + +SMNT: + +Este comando permite al usuario montar una estructura de datos de sistema de archivos diferente sin alterar su información de inicio de sesión o contabilidad. Los parámetros de transferencia también permanecen sin cambios. El argumento es una ruta que especifica un directorio o un designador de grupo de archivos dependiente del sistema. + + +REIN: + +Este comando termina una sesión de USUARIO, eliminando toda la información de entrada/salida y de cuenta, excepto para permitir que cualquier transferencia en progreso se complete. Todos los parámetros se restablecen a la configuración predeterminada y la conexión de control permanece abierta. Esto es idéntico al estado en que se encuentra un usuario inmediatamente después de que se abre la conexión de control. Se puede esperar que un comando USER siga a continuación. + + +QUIT: + +Este comando termina una sesión de USUARIO y, si no hay una transferencia de archivos en progreso, el servidor cierra la conexión de control. Si la transferencia de archivos está en progreso, la conexión permanecerá abierta para la respuesta del resultado y luego el servidor la cerrará. Si el proceso de usuario está transfiriendo archivos para varios USUARIOS pero no desea cerrar y luego reabrir las conexiones para cada uno, entonces se debe usar el comando REIN en lugar de QUIT. +Un cierre inesperado de la conexión de control hará que el servidor tome la acción efectiva de un aborto (ABOR) y una desconexión (QUIT). + + +PORT: + +El argumento es una especificación HOST-PORT para el puerto de datos que se utilizará en la conexión de datos. Existen valores predeterminados tanto para los puertos de datos del usuario como del servidor, y en circunstancias normales, este comando y su respuesta no son necesarios. Si se utiliza este comando, el argumento es la concatenación de una dirección de host de internet de 32 bits y una dirección de puerto TCP de 16 bits. Esta información de dirección se divide en campos de 8 bits y el valor de cada campo se transmite como un número decimal (en representación de cadena de caracteres). Los campos están separados por comas. Un comando de puerto sería: PORT h1,h2,h3,h4,p1,p2 donde h1 son los 8 bits de orden superior de la dirección del host de internet. + + +PASV: + +Este comando solicita al server-DTP que "escuche" en un puerto de datos (que no es su puerto de datos predeterminado) y que espere una conexión en lugar de iniciarla al recibir un comando de transferencia. La respuesta a este comando incluye la dirección del host y del puerto en el que el servidor está escuchando. + + +TYPE: + +El argumento especifica el tipo de representación como se describe en la Sección sobre Representación y Almacenamiento de Datos. Varios tipos requieren un segundo parámetro. El primer parámetro se denota por un solo carácter Telnet, al igual que el segundo parámetro de Formato para ASCII y EBCDIC; el segundo parámetro para byte local es un número entero decimal para indicar el tamaño de bytes. Los parámetros están separados por un (Espacio, código ASCII 32). +Los siguientes códigos se asignan para el tipo: + \ / + A - ASCII | | N - No imprimible + |-><-| T - Efectores de formato Telnet + E - EBCDIC| | C - Control de carro (ASA) + / \ + I - Imagen + + L - Byte local Tamaño de byte + +El tipo de representación predeterminado es ASCII No imprimible. Si el parámetro de Formato se cambia, y luego solo se cambia el primer argumento, el Formato vuelve al valor predeterminado de No imprimible. + + +STRU: + +El argumento es un código de carácter Telnet único que especifica la estructura del archivo descrita en la Sección sobre Representación y Almacenamiento de Datos. +Los siguientes códigos se asignan para la estructura: + F - Archivo (sin estructura de registros) + R - Estructura de registros + P - Estructura de páginas + + +MODE: + +El argumento es un código de carácter Telnet único que especifica los modos de transferencia de datos descritos en la Sección sobre Modos de Transmisión. +Los siguientes códigos se asignan para los modos de transferencia: + S - Flujo + B - Bloque + C - Comprimido + + +RETR: + +Este comando hace que el servidor-DTP transfiera una copia del archivo, especificado en la ruta, al servidor-DTP o usuario-DTP en el otro extremo de la conexión de datos. El estado y el contenido del archivo en el sitio del servidor no se verán afectados. + + +STOR: + +Este comando hace que el servidor-DTP acepte los datos transferidos a través de la conexión de datos y los almacene como un archivo en el sitio del servidor. Si el archivo especificado en la ruta ya existe en el sitio del servidor, su contenido será reemplazado por los datos que se están transfiriendo. Se crea un nuevo archivo en el sitio del servidor si el archivo especificado en la ruta no existe. + + +STOU: + +Este comando se comporta como STOR, excepto que el archivo resultante se creará en el directorio actual bajo un nombre único para ese directorio. La respuesta 250 Transfer Started debe incluir el nombre generado. + + + +APPE: + +Este comando hace que el servidor-DTP acepte los datos transferidos a través de la conexión de datos y los almacene en un archivo en el sitio del servidor. Si el archivo especificado en la ruta ya existe en el sitio del servidor, los datos se agregarán a ese archivo; de lo contrario, el archivo especificado en la ruta se creará en el sitio del servidor. + + +ALLO: + +Este comando puede ser requerido por algunos servidores para reservar suficiente almacenamiento para acomodar el nuevo archivo a ser transferido. El argumento será un número entero decimal que representa el número de bytes (usando el tamaño lógico de byte) de almacenamiento a ser reservado para el archivo. Para archivos enviados con estructura de registro o página, también podría ser necesario un tamaño máximo de registro o página (en bytes lógicos); esto se indica mediante un número entero decimal en un segundo campo de argumento del comando. Este segundo argumento es opcional, pero cuando esté presente, debe estar separado del primero por los tres caracteres Telnet R . Este comando debe ser seguido por un comando STORe o APPEnd. El comando ALLO debe ser tratado como un NOOP (sin operación) por aquellos servidores que no requieren que se declare el tamaño máximo del archivo de antemano, y aquellos servidores interesados solo en el tamaño máximo de registro o página deben aceptar un valor ficticio en el primer argumento e ignorarlo. + + +REST: + +El campo de argumento representa el marcador del servidor en el cual se debe reiniciar la transferencia de archivos. Este comando no provoca la transferencia de archivos, sino que se salta el archivo hasta el punto de control de datos especificado. Este comando debe ser seguido inmediatamente por el comando de servicio FTP apropiado que provoque la reanudación de la transferencia de archivos. + + +RNFR: + +Este comando especifica la ruta antigua del archivo que se va a renombrar. Este comando debe ser seguido inmediatamente por un comando "renombrar a" que especifique la nueva ruta del archivo. + + +RNTO: + +Este comando especifica la nueva ruta del archivo especificado en el comando "renombrar desde" inmediatamente anterior. Juntos, los dos comandos causan que se renombre un archivo. + + +ABOR: + +Este comando indica al servidor que aborte el comando de servicio FTP anterior y cualquier transferencia de datos asociada. El comando de abortar puede requerir "acción especial", como se discute en la Sección sobre Comandos FTP, para forzar el reconocimiento por parte del servidor. No se tomará ninguna acción si el comando anterior se ha completado (incluida la transferencia de datos). La conexión de control no debe ser cerrada por el servidor, pero la conexión de datos debe ser cerrada. +Hay dos casos para el servidor al recibir este comando: (1) el comando de servicio FTP ya se completó, o (2) el comando de servicio FTP todavía está en progreso. En el primer caso, el servidor cierra la conexión de datos (si está abierta) y responde con un código 226, indicando que el comando de abortar fue procesado con éxito. +En el segundo caso, el servidor aborta el servicio FTP en progreso y cierra la conexión de datos, devolviendo un código 426 para indicar que la solicitud de servicio terminó de manera anormal. Luego, el servidor envía un código 226, indicando que el comando de abortar fue procesado con éxito. + + +DELE: + +Este comando hace que el archivo especificado en la ruta sea eliminado en el sitio del servidor. Si se desea un nivel adicional de protección (como la consulta, "¿Realmente deseas eliminar?"), debe ser proporcionado por el proceso de usuario-FTP. + + +RMD: + +Este comando hace que el directorio especificado en la ruta sea eliminado como un directorio (si la ruta es absoluta) o como un subdirectorio del directorio de trabajo actual (si la ruta es relativa). + + +MKD: + +Este comando hace que el directorio especificado en la ruta sea creado como un directorio (si la ruta es absoluta) o como un subdirectorio del directorio de trabajo actual (si la ruta es relativa). + + +PWD: + +Este comando hace que el nombre del directorio de trabajo actual sea devuelto en la respuesta. + + +LIST: + +Este comando hace que una lista sea enviada desde el servidor al DTP pasivo. Si la ruta especifica un directorio u otro grupo de archivos, el servidor debe transferir una lista de archivos en el directorio especificado. Si la ruta especifica un archivo, el servidor debe enviar información actual sobre el archivo. Un argumento nulo implica el directorio de trabajo actual o predeterminado del usuario. La transferencia de datos se realiza a través de la conexión de datos en tipo ASCII o tipo EBCDIC. (El usuario debe asegurarse de que el TIPO sea apropiadamente ASCII o EBCDIC). Dado que la información sobre un archivo puede variar ampliamente de un sistema a otro, esta información puede ser difícil de usar automáticamente en un programa, pero puede ser bastante útil para un usuario humano. + + + +NLST: + +Este comando hace que se envíe una lista de directorios desde el servidor al sitio del usuario. La ruta debe especificar un directorio u otro descriptor de grupo de archivos específico del sistema; un argumento nulo implica el directorio actual. El servidor devolverá un flujo de nombres de archivos y ninguna otra información. Los datos se transferirán en tipo ASCII o EBCDIC sobre la conexión de datos como cadenas de rutas válidas separadas por o . (Nuevamente, el usuario debe asegurarse de que el TIPO sea correcto). Este comando está destinado a devolver información que puede ser utilizada por un programa para procesar automáticamente los archivos. Por ejemplo, en la implementación de una función "obtener múltiple". + + +SITE: + +Este comando es utilizado por el servidor para proporcionar servicios específicos de su sistema que son esenciales para la transferencia de archivos, pero no lo suficientemente universales como para ser incluidos como comandos en el protocolo. La naturaleza de estos servicios y la especificación de su sintaxis pueden ser indicadas en una respuesta al comando HELP SITE. + + +SYST: + +Este comando se utiliza para averiguar el tipo de sistema operativo en el servidor. La respuesta deberá tener como su primera palabra uno de los nombres de sistema enumerados en la versión actual del documento de Números Asignados. + + +STAT: + +Este comando hará que se envíe una respuesta de estado a través de la conexión de control en forma de una respuesta. El comando puede ser enviado durante una transferencia de archivos (junto con las señales Telnet IP y Synch; véase la Sección sobre Comandos FTP), en cuyo caso el servidor responderá con el estado de la operación en progreso, o puede ser enviado entre transferencias de archivos. En este último caso, el comando puede tener un campo de argumento. Si el argumento es una ruta, el comando es análogo al comando "listar", excepto que los datos se transferirán a través de la conexión de control. Si se da una ruta parcial, el servidor puede responder con una lista de nombres de archivos o atributos asociados con esa especificación. Si no se da ningún argumento, el servidor debe devolver información de estado general sobre el proceso FTP del servidor. Esto debe incluir valores actuales de todos los parámetros de transferencia y el estado de las conexiones. + + +HELP: + +Este comando hará que el servidor envíe información útil sobre su estado de implementación a través de la conexión de control al usuario. El comando puede tomar un argumento (por ejemplo, cualquier nombre de comando) y devolver información más específica como respuesta. La respuesta es de tipo 211 o 214. Se sugiere que se permita el comando HELP antes de ingresar un comando USER. El servidor puede usar esta respuesta para especificar parámetros dependientes del sitio, por ejemplo, en respuesta a HELP SITE. + + +NOOP: + +Este comando no afecta ningún parámetro ni comandos ingresados previamente. No especifica ninguna acción más allá de que el servidor envíe una respuesta de OK. \ No newline at end of file diff --git a/README.md b/README.md index 26e6bf0..222aaeb 100644 --- a/README.md +++ b/README.md @@ -1 +1,269 @@ -# computer_networks_fall_2024 \ No newline at end of file +# Servidor y Cliente FTP + +Implementación de un servidor y cliente FTP en Python, desarrollado como proyecto para la clase de Redes de Computadoras. + +## Características + +### Servidor +- Manejo de usuarios básico +- Operaciones con archivos y directorios +- Soporte para comandos FTP estándar +- Transferencia de archivos en modo ASCII y binario +- Sistema de permisos básico + +### Cliente +- Interfaz de línea de comandos interactiva +- Descarga automática de archivos a carpeta "Downloads" +- Soporte completo de comandos FTP +- Manejo de errores y feedback al usuario + +## Comandos Soportados + +### Comandos de Acceso y Control +- `USER` - Especifica el usuario +- `PASS` - Especifica la contraseña +- `ACCT` - Especifica la cuenta del usuario +- `QUIT` - Cierra la conexión +- `REIN` - Reinicia la conexión +- `NOOP` - No realiza ninguna operación + +### Comandos de Navegación +- `PWD` - Muestra el directorio actual +- `CWD` - Cambia el directorio de trabajo +- `CDUP` - Cambia al directorio padre +- `LIST` - Lista archivos y directorios +- `NLST` - Lista nombres de archivos + +### Comandos de Gestión de Archivos +- `MKD` - Crea un directorio +- `RMD` - Elimina un directorio +- `DELE` - Elimina un archivo +- `RNFR` - Especifica el archivo a renombrar +- `RNTO` - Especifica el nuevo nombre + +### Comandos de Transferencia +- `RETR` - Recupera/descarga un archivo +- `STOR` - Almacena/sube un archivo +- `STOU` - Almacena un archivo con nombre único +- `APPE` - Añade datos a un archivo existente +- `REST` - Reinicia transferencia desde un punto específico +- `ABOR` - Aborta la operación en progreso + +### Comandos de Configuración +- `PORT` - Especifica dirección y puerto para conexión +- `PASV` - Entra en modo pasivo +- `TYPE` - Establece el tipo de transferencia +- `STRU` - Establece la estructura de archivo +- `MODE` - Establece el modo de transferencia +- `ALLO` - Reserva espacio + +### Comandos de Información +- `SYST` - Muestra información del sistema +- `STAT` - Retorna estado actual +- `HELP` - Muestra la ayuda +- `SITE` - Comandos específicos del sitio +- `SMNT` - Monta una estructura de sistema de archivos + +## Uso + +### Iniciar el Servidor +```bash +python server.py +``` + +### Iniciar el Cliente +```bash +python client.py +``` + +### Ejemplos de Uso + +1. Conectar al servidor: + ``` + FTP> USER anonymous + FTP> PASS anonymous + ``` + +2. Navegar y listar archivos: + ``` + FTP> PWD + FTP> LIST + FTP> CWD carpeta + ``` + +3. Transferir archivos: + ``` + FTP> STOR archivo.txt + FTP> RETR documento.pdf + ``` + +## Requisitos +- Python 3.6+ +- Bibliotecas estándar de Python (socket, os, pathlib) +# 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` +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. +<<<<<<< HEAD +======= +# Servidor y Cliente FTP + +Implementación de un servidor y cliente FTP en Python, desarrollado como proyecto para la clase de Redes de Computadoras. + +## Características + +### Servidor +- Manejo de usuarios básico +- Operaciones con archivos y directorios +- Soporte para comandos FTP estándar +- Transferencia de archivos en modo ASCII y binario +- Sistema de permisos básico + +### Cliente +- Interfaz de línea de comandos interactiva +- Descarga automática de archivos a carpeta "Downloads" +- Soporte completo de comandos FTP +- Manejo de errores y feedback al usuario + +## Comandos Soportados + +### Comandos de Acceso y Control +- `USER` - Especifica el usuario +- `PASS` - Especifica la contraseña +- `ACCT` - Especifica la cuenta del usuario +- `QUIT` - Cierra la conexión +- `REIN` - Reinicia la conexión +- `NOOP` - No realiza ninguna operación + +### Comandos de Navegación +- `PWD` - Muestra el directorio actual +- `CWD` - Cambia el directorio de trabajo +- `CDUP` - Cambia al directorio padre +- `LIST` - Lista archivos y directorios +- `NLST` - Lista nombres de archivos + +### Comandos de Gestión de Archivos +- `MKD` - Crea un directorio +- `RMD` - Elimina un directorio +- `DELE` - Elimina un archivo +- `RNFR` - Especifica el archivo a renombrar +- `RNTO` - Especifica el nuevo nombre + +### Comandos de Transferencia +- `RETR` - Recupera/descarga un archivo +- `STOR` - Almacena/sube un archivo +- `STOU` - Almacena un archivo con nombre único +- `APPE` - Añade datos a un archivo existente +- `REST` - Reinicia transferencia desde un punto específico +- `ABOR` - Aborta la operación en progreso + +### Comandos de Configuración +- `PORT` - Especifica dirección y puerto para conexión +- `PASV` - Entra en modo pasivo +- `TYPE` - Establece el tipo de transferencia +- `STRU` - Establece la estructura de archivo +- `MODE` - Establece el modo de transferencia +- `ALLO` - Reserva espacio + +### Comandos de Información +- `SYST` - Muestra información del sistema +- `STAT` - Retorna estado actual +- `HELP` - Muestra la ayuda +- `SITE` - Comandos específicos del sitio +- `SMNT` - Monta una estructura de sistema de archivos + +## Uso + +### Iniciar el Servidor +```bash +python server.py +``` + +### Iniciar el Cliente +```bash +python client.py +``` + +### Ejemplos de Uso + +1. Conectar al servidor: + ``` + FTP> USER anonymous + FTP> PASS anonymous + ``` + +2. Navegar y listar archivos: + ``` + FTP> PWD + FTP> LIST + FTP> CWD carpeta + ``` + +3. Transferir archivos: + ``` + FTP> STOR archivo.txt + FTP> RETR documento.pdf + ``` + +## Requisitos +- Python 3.6+ +- Bibliotecas estándar de Python (socket, os, pathlib) +>>>>>>> 6f73f99 (feat:Added the logic of the commands in the client and some in the server) 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/run.sh b/run.sh index 475b295..a475101 100644 --- a/run.sh +++ b/run.sh @@ -1,4 +1,4 @@ -PROTOCOL=0 +#!/bin/bash # Replace the next shell command with the entrypoint of your solution 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..292a085 100644 --- a/tests/ftp/install.sh +++ b/tests/ftp/install.sh @@ -1 +1,3 @@ -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 +ip=$(hostname -I) +docker run -d --rm --name vsftpd -p 21:21 -p 21100-21110:21100-21110 -e "PASV_ADDRESS=${ip}" -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..92484e1 100644 --- a/tests/ftp/run.sh +++ b/tests/ftp/run.sh @@ -1 +1,22 @@ -./run.sh --user test --body test \ No newline at end of file +#!/bin/bash + +# Mostrar ayuda si no se proporcionan suficientes argumentos +if [ "$#" -lt 9 ]; then + echo "Uso: $0 -s server -p puerto -u user -pw pass \"command\"" + exit 1 +fi + +# Parsear los argumentos +while [[ "$#" -gt 0 ]]; do + case $1 in + -s) server="$2"; shift ;; + -p) port="$2"; shift ;; + -u) user="$2"; shift ;; + -pw) pass="$2"; shift ;; + *) command="$1" ;; + esac + shift +done + +# Ejecutar el script de Python con los parámetros proporcionados +python3 tester.py -s $server -p $port -u $user -pw $pass -c "$command" diff --git a/tests/ftp/tester.py b/tests/ftp/tester.py new file mode 100644 index 0000000..17b46d3 --- /dev/null +++ b/tests/ftp/tester.py @@ -0,0 +1,43 @@ +import subprocess, sys + +def make_test(args, expeteted_output, error_msg): + command = f"./run.sh {args}" + + output = subprocess.run([x for x in command.split(' ')], capture_output=True, text=True).stdout + + 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 localhost -p 21 -u user -w pass", ("220","230",), "Login Failed"), + ("-h localhost -p 21 -u user -w pass -c PWD", ("150","226",), "/ directory listing failed"), + ("-h localhost -p 21 -u user -w pass -c CWD -a /directory", ("250",), "change directory failed"), + ("-h localhost -p 21 -u user -w pass -c QUIT", ("221",), "exiting ftp server failed"), + ("-h localhost -p 21 -u user -w pass -c RETR -a 2.txt" , ("150","226",), "could not retrieve 2.txt file"), + ("-h localhost -p 21 -u user -w pass -c STOR -a tests/ftp/new.txt -b new.txt", ("150", "226",), "file new.txt upload failed"), + ("-h localhost -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 localhost -p 21 -u user -w pass -c DELE -a new.txt", ("250",), "delete new.txt failed"), + ("-h localhost -p 21 -u user -w pass -c MKD -a directory2", ("257",), "directory directory2 creation failed"), + ("-h localhost -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/run.sh b/tests/http/run.sh index 2270ba9..620852d 100644 --- a/tests/http/run.sh +++ b/tests/http/run.sh @@ -12,6 +12,7 @@ sleep 2 echo "Ejecutando las pruebas..." python3 ./tests/http/tests.py -# Detener el servidor después de las pruebas -echo "Deteniendo el servidor..." -kill $SERVER_PID +if [[ $? -ne 0 ]]; then + echo "HTTP test failed" + exit 1 +fi \ No newline at end of file diff --git a/tests/http/tests.py b/tests/http/tests.py index eb8ef84..e1b40f4 100644 --- a/tests/http/tests.py +++ b/tests/http/tests.py @@ -1,10 +1,11 @@ -import os +import os, sys +import json def make_request(method, path, headers=None, data=None): - headerstr = "" if headers is None else f" -h {headers}" - datastr = "" if data is None else f" -b {data}" - response_string = os.popen(f"run.sh -m {method} -u http://localhost:8080/{path} -h {headerstr} -d {datastr}").read() - return response_string # JSON con campos status, body y headers + 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 = [] @@ -47,11 +48,11 @@ def evaluate_response(case, expected_status, actual_status, expected_body=None, 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"}') +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"}') +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 @@ -81,7 +82,7 @@ def evaluate_response(case, expected_status, actual_status, expected_body=None, response = make_request( "POST", "/secure", - headers='{"Authorization": "Bearer 12345", "Content-Type": "application/json"}', + headers='{\\"Authorization\\":\\ \\"Bearer\\ 12345\\",\\ \\"Content-Type\\":\\ \\"application/json\\"}', data='{"key":}' ) evaluate_response( @@ -97,7 +98,7 @@ def evaluate_response(case, expected_status, actual_status, expected_body=None, response = make_request( "POST", "/secure", - headers='{"Content-Type": "application/json"}', + headers='{\\"Content-Type\\":\\ \\"application/json\\"}', data='{"key":}' ) evaluate_response( @@ -130,3 +131,4 @@ def evaluate_response(case, expected_status, actual_status, expected_body=None, 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..ed5d563 --- /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 sent successfully", 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 sent successfully", 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 sent successfully", 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 sent successfully", 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 sent successfully", 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 sent successfully", 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 sent successfully", 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) \ No newline at end of file