""" FTP Session Handler - Manages individual client connections """ import socket import threading import os import time import random from config import FTP_RESPONSES, USERS, SERVER_ROOT, DATA_PORT_RANGE class FTPSession: def __init__(self, client_socket, client_address): self.client_socket = client_socket self.client_address = client_address self.data_socket = None self.data_address = None self.passive_socket = None # Session state self.authenticated = False self.username = None self.current_directory = SERVER_ROOT self.transfer_mode = 'I' # Binary mode by default # Ensure server root directory exists if not os.path.exists(SERVER_ROOT): os.makedirs(SERVER_ROOT) print(f"New FTP session started for {client_address}") def send_response(self, code, message=None): """Send FTP response to client""" if message is None: response = FTP_RESPONSES.get(code, f"{code} Unknown response code.") else: response = f"{code} {message}" try: self.client_socket.send((response + '\r\n').encode('utf-8')) print(f"Sent to {self.client_address}: {response}") except Exception as e: print(f"Error sending response to {self.client_address}: {e}") def handle_session(self): """Main session handler""" try: # Send welcome message self.send_response('220', 'FTP Server Ready') while True: try: # Receive command from client data = self.client_socket.recv(1024).decode('utf-8').strip() if not data: break print(f"Received from {self.client_address}: {data}") # Parse command parts = data.split(' ', 1) command = parts[0].upper() args = parts[1] if len(parts) > 1 else '' # Handle command self.handle_command(command, args) except ConnectionResetError: print(f"Client {self.client_address} disconnected") break except Exception as e: print(f"Error handling command from {self.client_address}: {e}") self.send_response('500', 'Internal server error') except Exception as e: print(f"Session error for {self.client_address}: {e}") finally: self.cleanup() def handle_command(self, command, args): """Route commands to appropriate handlers""" if command == 'USER': self.handle_user(args) elif command == 'PASS': self.handle_pass(args) elif command == 'QUIT': self.handle_quit() elif command == 'PWD': self.handle_pwd() elif command == 'CWD': self.handle_cwd(args) elif command == 'LIST' or command == 'NLST': self.handle_list(command) elif command == 'RETR': self.handle_retr(args) elif command == 'PASV': self.handle_pasv() elif command == 'PORT': self.handle_port(args) elif command == 'TYPE': self.handle_type(args) elif command == 'SYST': self.handle_syst() elif command == 'NOOP': self.send_response('200') else: self.send_response('500', f'Command {command} not recognized') def handle_user(self, username): """Handle USER command""" if not username: self.send_response('501', 'Username required') return self.username = username self.authenticated = False if username in USERS: self.send_response('331', f'User {username} OK. Password required') else: self.send_response('331', 'Password required') # Don't reveal if user exists def handle_pass(self, password): """Handle PASS command""" if not self.username: self.send_response('503', 'Login with USER first') return if not password: self.send_response('501', 'Password required') return if self.username in USERS and USERS[self.username] == password: self.authenticated = True self.send_response('230', f'User {self.username} logged in') else: self.authenticated = False self.send_response('530', 'Login incorrect') def handle_quit(self): """Handle QUIT command""" self.send_response('221', 'Goodbye') self.client_socket.close() return True # Signal to exit the session loop def handle_pwd(self): """Handle PWD command""" if not self.authenticated: self.send_response('530', 'Not logged in') return # Return relative path from server root rel_path = os.path.relpath(self.current_directory, SERVER_ROOT) if rel_path == '.': rel_path = '/' else: rel_path = '/' + rel_path.replace('\\', '/') self.send_response('257', f'"{rel_path}" is current directory') def handle_cwd(self, path): """Handle CWD command""" if not self.authenticated: self.send_response('530', 'Not logged in') return if not path: self.send_response('501', 'Path required') return # Handle absolute and relative paths if path.startswith('/'): new_path = os.path.join(SERVER_ROOT, path.lstrip('/')) else: new_path = os.path.join(self.current_directory, path) new_path = os.path.normpath(new_path) # Security check: ensure path is within server root if not new_path.startswith(SERVER_ROOT): self.send_response('550', 'Permission denied') return if os.path.exists(new_path) and os.path.isdir(new_path): self.current_directory = new_path self.send_response('250', 'Directory changed') else: self.send_response('550', 'Directory not found') def handle_syst(self): """Handle SYST command""" self.send_response('215', 'UNIX Type: L8') def handle_type(self, type_arg): """Handle TYPE command""" if not self.authenticated: self.send_response('530', 'Not logged in') return if type_arg.upper() in ['I', 'A']: self.transfer_mode = type_arg.upper() self.send_response('200', f'Type set to {type_arg.upper()}') else: self.send_response('501', 'Type not supported') def handle_pasv(self): """Handle PASV command - Enter passive mode""" if not self.authenticated: self.send_response('530', 'Not logged in') return try: # Close existing passive socket if any if self.passive_socket: self.passive_socket.close() # Create new passive socket self.passive_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.passive_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # Find available port for port in range(DATA_PORT_RANGE[0], DATA_PORT_RANGE[1]): try: self.passive_socket.bind(('127.0.0.1', port)) break except OSError: continue else: self.send_response('425', 'Cannot open passive connection') return self.passive_socket.listen(1) # Calculate port representation for PASV response p1 = port // 256 p2 = port % 256 self.send_response('227', f'Entering Passive Mode (127,0,0,1,{p1},{p2})') except Exception as e: print(f"Error setting up passive mode: {e}") self.send_response('425', 'Cannot open passive connection') def handle_port(self, args): """Handle PORT command - Active mode""" if not self.authenticated: self.send_response('530', 'Not logged in') return try: # Parse PORT command arguments parts = args.split(',') if len(parts) != 6: self.send_response('501', 'Invalid PORT command') return # Extract IP and port ip = '.'.join(parts[:4]) port = int(parts[4]) * 256 + int(parts[5]) self.data_address = (ip, port) self.send_response('200', 'PORT command successful') except Exception as e: print(f"Error parsing PORT command: {e}") self.send_response('501', 'Invalid PORT command') def establish_data_connection(self): """Establish data connection (passive or active mode)""" try: if self.passive_socket: # Passive mode self.data_socket, _ = self.passive_socket.accept() return True elif self.data_address: # Active mode self.data_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.data_socket.connect(self.data_address) return True else: return False except Exception as e: print(f"Error establishing data connection: {e}") return False def close_data_connection(self): """Close data connection""" try: if self.data_socket: self.data_socket.close() self.data_socket = None except: pass def handle_list(self, command): """Handle LIST/NLST commands""" if not self.authenticated: self.send_response('530', 'Not logged in') return try: self.send_response('150', 'Opening data connection for directory list') if not self.establish_data_connection(): self.send_response('425', 'Cannot open data connection') return # Get directory listing try: files = os.listdir(self.current_directory) listing = [] for filename in files: filepath = os.path.join(self.current_directory, filename) stat = os.stat(filepath) if command == 'LIST': # Detailed listing is_dir = 'd' if os.path.isdir(filepath) else '-' permissions = 'rwxr-xr-x' # Simplified permissions size = stat.st_size mtime = time.strftime('%b %d %H:%M', time.localtime(stat.st_mtime)) listing.append(f'{is_dir}{permissions} 1 user user {size:8} {mtime} {filename}') else: # Simple listing (NLST) listing.append(filename) # Send listing data = '\r\n'.join(listing) + '\r\n' self.data_socket.send(data.encode('utf-8')) self.close_data_connection() self.send_response('226', 'Directory listing completed') except Exception as e: print(f"Error reading directory: {e}") self.close_data_connection() self.send_response('550', 'Directory listing failed') except Exception as e: print(f"Error in LIST command: {e}") self.send_response('425', 'Cannot open data connection') def handle_retr(self, filename): """Handle RETR command - Download file""" if not self.authenticated: self.send_response('530', 'Not logged in') return if not filename: self.send_response('501', 'Filename required') return # Construct file path filepath = os.path.join(self.current_directory, filename) filepath = os.path.normpath(filepath) # Security check if not filepath.startswith(SERVER_ROOT): self.send_response('550', 'Permission denied') return if not os.path.exists(filepath) or not os.path.isfile(filepath): self.send_response('550', 'File not found') return try: self.send_response('150', f'Opening data connection for {filename}') if not self.establish_data_connection(): self.send_response('425', 'Cannot open data connection') return # Send file with open(filepath, 'rb') as f: while True: data = f.read(8192) if not data: break self.data_socket.send(data) self.close_data_connection() self.send_response('226', 'File transfer completed') except Exception as e: print(f"Error transferring file {filename}: {e}") self.close_data_connection() self.send_response('550', 'File transfer failed') def cleanup(self): """Clean up session resources""" try: if self.data_socket: self.data_socket.close() if self.passive_socket: self.passive_socket.close() if self.client_socket: self.client_socket.close() except: pass print(f"Session ended for {self.client_address}")