diff --git a/fakeftp.py b/fakeftp.py index f07ef68..aa7dcd8 100644 --- a/fakeftp.py +++ b/fakeftp.py @@ -3,28 +3,38 @@ import threading import socket from random import randint import datetime +class Utils: + @staticmethod + def mask(rights): + m = "" + if rights&4 != 0: + m += "r" + else: + m += "-" -def mask(rights): - m = "" - if rights&4 != 0: - m += "r" - else: - m += "-" + if rights&2 != 0: + m += "w" + else: + m += "-" - if rights&2 != 0: - m += "w" - else: - m += "-" + if rights&1 != 0: + m += "x" + else: + m+= "-" - if rights&1 != 0: - m += "x" - else: - m+= "-" + return m - return m + @staticmethod + def _r( (u,g,o) ): + return ( (u&7) << 6 ) + ( (g&7) << 3 ) + (o&7) -def _r( (u,g,o) ): - return ( (u&7) << 6 ) + ( (g&7) << 3 ) + (o&7) +class Logger: + def log(self, handler, message): + pass + +class StdoutLogger: + def log(self, handler, message): + print "[%s] %s" % (handler.client_address[0],message) class FSNode: def __init__(self,name,owner="nobody",group="nobody",rights=(6,4,4),size=0, iun=1): @@ -33,11 +43,11 @@ class FSNode: self.group = group if type(rights) == tuple and len(rights) == 3: - self.rights = _r( rights ) + self.rights = Utils._r( rights ) elif type(rights) == int: self.rights = rights else: - self.rights = _r( (6,4,4) ) + self.rights = Utils._r( (6,4,4) ) self.size = size @@ -86,9 +96,9 @@ class FSNode: m += "-" rights = self.get_rights() - m += mask((rights >> 6)&7) - m += mask((rights >> 3)&7) - m += mask(rights&7) + m += Utils.mask((rights >> 6)&7) + m += Utils.mask((rights >> 3)&7) + m += Utils.mask(rights&7) return m @@ -340,6 +350,10 @@ class SystemTypeMessage(FTPMessage): def __init__(self, sys="UNIX",typ="L8"): FTPMessage.__init__(self,"215","%s Type: %s" % ( sys.upper(), typ )) +class FatalServerErrorMessage(FTPMessage): + def __init__(self): + FTPMessage.__init__(self, "421", "Fatal server error") + # Data Connection Handler class NListHandler: def __init__(self, directory, handler): @@ -381,148 +395,246 @@ class RetrieveHandler: self.node.send_content(socket) self.handler.send( EndFileRetrieve() ) -# State classes -class State: +# CommandHandler classes +class CommandHandler: + cmd = None def process(self, handler, message): pass -class MainState(State): +class ForbiddenCommandHandler(CommandHandler): def process(self, handler, message): - if message.cmd == "QUIT": - handler.debug( "Quitting" ) - handler.send( QuitMessage() ) - handler.running = False + if hasattr(self,'log'): + handler.log(self.log % message.parameter) + handler.send( OperationNotPermittedMessage() ) + +class UnknownCommandHandler(CommandHandler): + def process(self, handler, message): + handler.log("Unknown command %s" % message.cmd) + handler.send( InvalidCommandMessage(message.cmd) ) + +class QuitCommandHandler(CommandHandler): + cmd = "QUIT" + def process(self, handler, message): + handler.log("Client quits") + handler.send( QuitMessage() ) + handler.running = False + +class UserCommandHandler(CommandHandler): + cmd = "USER" + def process(self, handler, message): + handler.log("Setting user to %s" % message.parameter) + handler.user = message.parameter + handler.state = AuthorizingState() + handler.send( PasswordRequiredMessage(handler.user) ) + +class PassCommandHandler(CommandHandler): + cmd = "PASS" + def process(self, handler, message): + password = message.parameter + if handler.authorizer.authorize(handler.user, password, handler): + handler.log("User login %s successful using password %s" % (handler.user,password)) + handler.state = AuthorizedState() + handler.send( AuthorizationSuccessfulMessage() ) else: - handler.send( InvalidCommandMessage(message.cmd) ) + handler.log("User login %s failed using password %s" % (handler.user, password) ) + handler.state = NonAuthorizedState() + handler.send( AuthorizationFailedMessage() ) -class NonAuthorizedState(MainState): +class PasvCommandHandler(CommandHandler): + cmd = "PASV" def process(self, handler, message): - if message.cmd == "USER": - handler.debug("Setting user to %s" % message.parameter) - handler.user = message.parameter - handler.state = AuthorizingState() - handler.send( PasswordRequiredMessage(handler.user) ) - else: - MainState.process(self, handler, message) + addr = handler.createPassiveDataConnection() + handler.log("Create passive data connection") + handler.send( EnteringPassiveModeMessage( addr ) ) -class AuthorizingState(MainState): +class PortCommandHandler(CommandHandler): + cmd = "PORT" def process(self, handler, message): - if message.cmd == "USER": - handler.debug("Changing user to %s" % message.parameter) - handler.user = message.parameter - handler.send( PasswordRequiredMessage(handler.user) ) - elif message.cmd == "PASS": - password = message.parameter - if handler.authorizer.authorize(handler.user, password, handler): - handler.debug("User login %s successful using password %s" % (handler.user,password)) - handler.state = AuthorizedState() - handler.send( AuthorizationSuccessfulMessage() ) - else: - handler.debug("User login %s failed using password %s" % (handler.user, password) ) - handler.state = NonAuthorizedState() - handler.send( AuthorizationFailedMessage() ) - else: - MainState.process(self, handler, message) + hp = message.parameter.split(",") + if len(hp) != 6: + handler.send( InvalidParametersMessage() ) + return -class AuthorizedState(MainState): + if int(hp[0]) == 10 or int(hp[0]) == 127 or int(hp[0]) == 192: #Prevent local scanning + handler.log( "Attempt to port to local network." ) + handler.send( InvalidPortCommandMessage() ) + return + + addr = ( ".".join(hp[:4]), int(hp[4])<<8+int(hp[5]) ) + + handler.log( "Creating active data connection to %s:%d" % addr ) + + handler.createActiveDataConnection(addr) + + handler.send( CommandSuccessfulMessage("PORT") ) + +class DeleteCommandHandler(ForbiddenCommandHandler): + cmd = "DELE" + log = "Attempt to delete file %s" + +class RemoveDirCommandHandler(ForbiddenCommandHandler): + cmd = "RMD" + log = "Attempt to delete directory %s" + +class RenameFromCommandHandler(ForbiddenCommandHandler): + cmd = "RNFR" + log = "Attempt to rename %s" + +class RenameToCommandHandler(ForbiddenCommandHandler): + cmd = "RNTO" + log = "Attempt to rename to %s" + +class StoreCommandHandler(ForbiddenCommandHandler): + cmd = "STOR" + log = "Attempt to store file %s" + +class RetrieveCommandHandler(CommandHandler): + cmd = "RETR" def process(self, handler, message): - if message.cmd == "PASV": - addr = handler.createPassiveDataConnection() - handler.debug("Created passive data connection") - handler.send( EnteringPassiveModeMessage( addr ) ) - elif message.cmd == "DELE": - handler.debug( "Attempt to delete file %s" % message.parameter ) - handler.send( OperationNotPermittedMessage() ) - elif message.cmd == "RMD": - handler.debug( "Attempt to delete directory %s" % message.parameter ) - handler.send( OperationNotPermittedMessage() ) - elif message.cmd == "RNFR": - handler.debug( "Attempt to rename %s" % message.parameter ) - handler.send( OperationNotPermittedMessage() ) - elif message.cmd == "RNTO": - handler.debug( "Attempt to rename to %s" % message.parameter ) - handler.send( OperationNotPermittedMessage() ) - elif message.cmd == "STOR": - handler.debug( "Attempt to store a file %s" % message.parameter ) - handler.send( OperationNotPermittedMessage() ) - elif message.cmd == "RETR": - node = handler.get_node(message.parameter) - if node.is_file(): - if handler.runDataConnection( RetrieveHandler(node, handler) ): - handler.debug( "Retrieving file %s" % message.parameter ) - else: - handler.send( UnableToBuildConnectionMessage() ) - else: - handler.send( OperationNotPermittedMessage() ) - elif message.cmd == "PORT": - hp = message.parameter.split(",") - if len(hp) != 6: - handler.send( InvalidParametersMessage() ) - return - - if int(hp[0]) == 10 or int(hp[0]) == 127 or int(hp[0]) == 192: #Prevent local scanning - handler.debug( "Attempt to port to local network." ) - handler.send( InvalidPortCommandMessage() ) - return - - addr = ( ".".join(hp[:4]), int(hp[4])<<8+int(hp[5]) ) - - handler.debug( "Creating active data connection to %s:%d" % addr ) - - handler.createActiveDataConnection(addr) - - handler.send( CommandSuccessfulMessage("PORT") ) - elif message.cmd == "LIST": - if handler.runDataConnection(ListHandler(handler.currentDirectory, handler)): - handler.debug("Listing current directory") + node = handler.get_node(message.parameter) + if node.is_file(): + if handler.runDataConnection( RetrieveHandler(node, handler) ): + handler.log( "Retrieving file %s" % message.parameter ) else: handler.send( UnableToBuildConnectionMessage() ) - elif message.cmd == "NLST": - if handler.runDataConnection(NListHandler(handler.currentDirectory)): - handler.debug("Listing current directory (simple)") - else: - handler.send( UnableToBuildConnectionMessage() ) - elif message.cmd == "CWD": - node = handler.get_node(message.parameter) - if node is None or not node.is_dir(): - handler.debug("Error trying to switch into non-existent directory") - handler.send( ChangeDirectoryFailedMessage() ) - else: - handler.currentDirectory = node - handler.debug("Changed working directory to "+node.get_absolute_path()) - handler.send( ChangeDirectorySuccessfulMessage() ) - elif message.cmd == "CDUP": - node = handler.currentDirectory.get_parent_directory() - if node is None or not node.is_dir(): - handler.debug("Error trying to switch to non-existent parent directory") - else: - handler.debug("Changed working directory to "+node.get_absolute_path()) - handler.currentDirectory = node + else: + handler.send( OperationNotPermittedMessage() ) + +class ListCommandHandler(CommandHandler): + cmd = "LIST" + def process(self, handler, message): + if handler.runDataConnection(ListHandler(handler.currentDirectory, handler)): + handler.log("Listing current directory") + else: + handler.send( UnableToBuildConnectionMessage() ) + +class SimpleListCommandHandler(CommandHandler): + cmd = "NLST" + def process(self, handler, message): + if handler.runDataConnection(NListHandler(handler.currentDirectory)): + handler.log("Listing current directory (simple)") + else: + handler.send( UnableToBuildConnectionMessage() ) + +class ChangeWorkingDirectoryCommandHandler(CommandHandler): + cmd = "CWD" + def process(self, handler, message): + node = handler.get_node(message.parameter) + if node is None or not node.is_dir(): + handler.log("Error trying to switch into non-existent directory") + handler.send( ChangeDirectoryFailedMessage() ) + else: + handler.currentDirectory = node + handler.log("Changed working directory to "+node.get_absolute_path()) handler.send( ChangeDirectorySuccessfulMessage() ) - elif message.cmd == "PWD": - handler.debug( "Printing working directory" ) - handler.send( WorkingDirectoryMessage(handler.currentDirectory.get_absolute_path()) ) - elif message.cmd == "TYPE": - if message.parameter == "A": - handler.debug("Setting type to ASCII") - handler.send( TypeSetMessage(message.parameter) ) - else: - handler.debug("Unrecognized type %s" % message.parameter) - handler.send( InvalidCommandMessage("TYPE") ) - elif message.cmd == "NOOP": - handler.debug("No operation") - handler.send( CommandSuccessfulMessage("NOOP") ) - elif message.cmd == "SIZE": - handler.debug("Requested size of %s" % message.parameter) - handler.send( CommandNotAllowedInAscii("SIZE") ) - elif message.cmd == "ABOR": - handler.debug("Aborting data connection") - handler.send( AbortSuccessfulMessage() ) - elif message.cmd == "SYST": - handler.debug("Requested system type") - handler.send( SystemTypeMessage() ) + +class ChangeDirectoryUpCommandHandler(CommandHandler): + cmd = "CDUP" + def process(self, handler, message): + node = handler.currentDirectory.get_parent_directory() + if node is None or not node.is_dir(): + handler.log("Error trying to switch to non-existent parent directory") else: - MainState.process(self, handler, message) + handler.log("Changed working directory to "+node.get_absolute_path()) + handler.currentDirectory = node + handler.send( ChangeDirectorySuccessfulMessage() ) + +class PrintWorkingDirectoryCommandHandler(CommandHandler): + cmd = "PWD" + def process(self, handler, message): + handler.log( "Printing working directory" ) + handler.send( WorkingDirectoryMessage(handler.currentDirectory.get_absolute_path()) ) + +class SetTypeCommandHandler(CommandHandler): + cmd = "TYPE" + def process(self, handler, message): + if message.parameter == "A": + handler.log("Setting type to ASCII") + handler.send( TypeSetMessage(message.parameter) ) + else: + handler.log("Unrecognized type %s" % message.parameter) + handler.send( InvalidCommandMessage("TYPE") ) + +class NoOperationCommandHandler(CommandHandler): + cmd = "NOOP" + def process(self, handler, message): + handler.log("No operation") + handler.send( CommandSuccessfulMessage("NOOP") ) + +class SizeCommandHandler(CommandHandler): + cmd = "SIZE" + def process(self, handler, message): + handler.log("Requested size of %s" % message.parameter) + handler.send( CommandNotAllowedInAscii("SIZE") ) + +class AbortCommandHandler(CommandHandler): + cmd = "ABOR" + def process(self, handler, message): + handler.log("Aborting data connection") + handler.send( AbortSuccessfulMessage() ) + +class SystemTypeCommandHandler(CommandHandler): + cmd = "SYST" + def process(self, handler, message): + handler.log("Requested system type") + handler.send( SystemTypeMessage() ) + +# State classes +class State: + handlers = [] + defaulthandler = UnknownCommandHandler() + + @classmethod + def get_handlers(cls): + hlds = [] + cls.handlers + for b in cls.__bases__: + hlds += b.get_handlers() + return hlds + + @classmethod + def get_handler(cls, cmd): + for hld in cls.get_handlers(): + if hld.cmd == cmd: + return hld + return cls.defaulthandler + + def process(self, handler, message): + try: + self.get_handler(message.cmd).process(handler,message) + except Exception: + handler.send( FatalServerErrorMessage() ) + handler.running = False + + +class BaseState(State): + handlers = [ QuitCommandHandler(), NoOperationCommandHandler() ] + +class NonAuthorizedState(BaseState): + handlers = [ UserCommandHandler() ] + +class AuthorizingState(NonAuthorizedState): + handlers = [ PassCommandHandler() ] + +class AuthorizedState(BaseState): + handlers = [ + PasvCommandHandler(), + PortCommandHandler(), + DeleteCommandHandler(), + RemoveDirCommandHandler(), + RenameToCommandHandler(), + RenameFromCommandHandler(), + StoreCommandHandler(), + RetrieveCommandHandler(), + ListCommandHandler(), + SimpleListCommandHandler(), + ChangeWorkingDirectoryCommandHandler(), + ChangeDirectoryUpCommandHandler(), + PrintWorkingDirectoryCommandHandler(), + SetTypeCommandHandler(), + SizeCommandHandler(), + AbortCommandHandler(), + SystemTypeCommandHandler() + ] class LineReader: def __init__(self): @@ -542,8 +654,10 @@ class LineReader: pos = self.buf.find("\n") class FTPHandler(SocketServer.BaseRequestHandler): - def debug(self, text): - print ("[%s] " % self.client_address[0]) + text + logger = StdoutLogger() + + def log(self, message): + self.logger.log(self,message) def send(self, message): self.request.send( str(message) ) @@ -594,7 +708,7 @@ class FTPHandler(SocketServer.BaseRequestHandler): self.state = NonAuthorizedState() - self.debug("Client connected"); + self.log("Client connected"); self.send( WelcomeMessage() ) @@ -613,7 +727,7 @@ class FTPHandler(SocketServer.BaseRequestHandler): message = FTPMessage.parse(line) self.state.process(self,message) - self.debug("Client disconnected") + self.log("Client disconnected") handler = FTPHandler handler.authorizer = AnonymousAuthorizer()