CN-design-ftp/ftp_session.py

404 lines
14 KiB
Python

"""
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}")