#!/usr/bin/python # This file is part of Fail2Ban. # # Fail2Ban is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # Fail2Ban is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Fail2Ban; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Author: Cyril Jaquier # # $Revision: 672 $ __author__ = "Cyril Jaquier" __version__ = "$Revision: 672 $" __date__ = "$Date: 2008-03-06 00:18:06 +0100 (Thu, 06 Mar 2008) $" __copyright__ = "Copyright (c) 2004 Cyril Jaquier" __license__ = "GPL" import sys, string, os, pickle, re, logging, signal import getopt, time, shlex, socket # Inserts our own modules path first in the list # fix for bug #343821 sys.path.insert(1, "/usr/share/fail2ban") # Now we can import our modules from common.version import version from common.protocol import printFormatted from client.csocket import CSocket from client.configurator import Configurator from client.beautifier import Beautifier # Gets the instance of the logger. logSys = logging.getLogger("fail2ban.client") ## # # @todo This class needs cleanup. class Fail2banClient: SERVER = "fail2ban-server" PROMPT = "fail2ban> " def __init__(self): self.__argv = None self.__stream = None self.__configurator = Configurator() self.__conf = dict() self.__conf["conf"] = "/etc/fail2ban" self.__conf["dump"] = False self.__conf["force"] = False self.__conf["verbose"] = 1 self.__conf["interactive"] = False self.__conf["socket"] = None def dispVersion(self): print "Fail2Ban v" + version print print "Copyright (c) 2004-2008 Cyril Jaquier" print "Copyright of modifications held by their respective authors." print "Licensed under the GNU General Public License v2 (GPL)." print print "Written by Cyril Jaquier <cyril.jaquier@fail2ban.org>." print "Many contributions by Yaroslav O. Halchenko <debian@onerussian.com>." def dispUsage(self): """ Prints Fail2Ban command line options and exits """ print "Usage: "+self.__argv[0]+" [OPTIONS] <COMMAND>" print print "Fail2Ban v" + version + " reads log file that contains password failure report" print "and bans the corresponding IP addresses using firewall rules." print print "Options:" print " -c <DIR> configuration directory" print " -s <FILE> socket path" print " -d dump configuration. For debugging" print " -i interactive mode" print " -v increase verbosity" print " -q decrease verbosity" print " -x force execution of the server (remove socket file)" print " -h, --help display this help message" print " -V, --version print the version" print print "Command:" # Prints the protocol printFormatted() print print "Report bugs to <cyril.jaquier@fail2ban.org>" def dispInteractive(self): print "Fail2Ban v" + version + " reads log file that contains password failure report" print "and bans the corresponding IP addresses using firewall rules." print def __sigTERMhandler(self, signum, frame): # Print a new line because we probably come from wait print logSys.warn("Caught signal %d. Exiting" % signum) sys.exit(-1) def __getCmdLineOptions(self, optList): """ Gets the command line options """ for opt in optList: if opt[0] == "-c": self.__conf["conf"] = opt[1] elif opt[0] == "-s": self.__conf["socket"] = opt[1] elif opt[0] == "-d": self.__conf["dump"] = True elif opt[0] == "-v": self.__conf["verbose"] = self.__conf["verbose"] + 1 elif opt[0] == "-q": self.__conf["verbose"] = self.__conf["verbose"] - 1 elif opt[0] == "-x": self.__conf["force"] = True elif opt[0] == "-i": self.__conf["interactive"] = True elif opt[0] in ["-h", "--help"]: self.dispUsage() sys.exit(0) elif opt[0] in ["-V", "--version"]: self.dispVersion() sys.exit(0) def __ping(self): return self.__processCmd([["ping"]], False) def __processCmd(self, cmd, showRet = True): beautifier = Beautifier() for c in cmd: beautifier.setInputCmd(c) try: client = CSocket(self.__conf["socket"]) ret = client.send(c) if ret[0] == 0: logSys.debug("OK : " + `ret[1]`) if showRet: print beautifier.beautify(ret[1]) else: logSys.debug("NOK: " + `ret[1].args`) print beautifier.beautifyError(ret[1]) return False except socket.error: if showRet: logSys.error("Unable to contact server. Is it running?") return False except Exception, e: if showRet: logSys.error(e) return False return True ## # Process a command line. # # Process one command line and exit. # @param cmd the command line def __processCommand(self, cmd): if len(cmd) == 1 and cmd[0] == "start": if self.__ping(): logSys.error("Server already running") return False else: # Read the config ret = self.__readConfig() # Do not continue if configuration is not 100% valid if not ret: return False # Start the server self.__startServerAsync(self.__conf["socket"], self.__conf["force"]) try: # Wait for the server to start self.__waitOnServer() # Configure the server self.__processCmd(self.__stream, False) return True except ServerExecutionException: logSys.error("Could not start server. Maybe an old " + "socket file is still present. Try to " + "remove " + self.__conf["socket"] + ". If " + "you used fail2ban-client to start the " + "server, adding the -x option will do it") return False elif len(cmd) == 1 and cmd[0] == "reload": if self.__ping(): ret = self.__readConfig() # Do not continue if configuration is not 100% valid if not ret: return False self.__processCmd([['stop', 'all']], False) # Configure the server return self.__processCmd(self.__stream, False) else: logSys.error("Could not find server") return False elif len(cmd) == 2 and cmd[0] == "reload": if self.__ping(): jail = cmd[1] ret = self.__readJailConfig(jail) # Do not continue if configuration is not 100% valid if not ret: return False self.__processCmd([['stop', jail]], False) # Configure the server return self.__processCmd(self.__stream, False) else: logSys.error("Could not find server") return False else: return self.__processCmd([cmd]) ## # Start Fail2Ban server. # # Start the Fail2ban server in daemon mode. def __startServerAsync(self, socket, force = False): # Forks the current process. pid = os.fork() if pid == 0: args = list() args.append(self.SERVER) # Start in background mode. args.append("-b") # Set the socket path. args.append("-s") args.append(socket) # Force the execution if needed. if force: args.append("-x") try: # Use the current directory. exe = os.path.abspath(os.path.join(sys.path[0], self.SERVER)) os.execv(exe, args) except OSError: try: # Use the PATH env. os.execvp(self.SERVER, args) except OSError: print "Could not find %s" % self.SERVER os.exit(-1) def __waitOnServer(self): # Wait for the server to start cnt = 0 if self.__conf["verbose"] > 1: pos = 0 delta = 1 mask = "[ ]" while not self.__ping(): # Wonderful visual :) if self.__conf["verbose"] > 1: pos += delta sys.stdout.write("\rINFO " + mask[:pos] + '#' + mask[pos+1:] + " Waiting on the server...") sys.stdout.flush() if pos > len(mask)-3: delta = -1 elif pos < 2: delta = 1 # The server has 30 secondes to start. if cnt >= 300: if self.__conf["verbose"] > 1: sys.stdout.write('\n') raise ServerExecutionException("Failed to start server") time.sleep(0.1) cnt += 1 if self.__conf["verbose"] > 1: sys.stdout.write('\n') def start(self, argv): # Command line options self.__argv = argv # Install signal handlers signal.signal(signal.SIGTERM, self.__sigTERMhandler) signal.signal(signal.SIGINT, self.__sigTERMhandler) # Reads the command line options. try: cmdOpts = 'hc:s:xdviqV' cmdLongOpts = ['help', 'version'] optList, args = getopt.getopt(self.__argv[1:], cmdOpts, cmdLongOpts) except getopt.GetoptError: self.dispUsage() return False self.__getCmdLineOptions(optList) verbose = self.__conf["verbose"] if verbose <= 0: logSys.setLevel(logging.ERROR) elif verbose == 1: logSys.setLevel(logging.WARN) elif verbose == 2: logSys.setLevel(logging.INFO) else: logSys.setLevel(logging.DEBUG) # Add the default logging handler stdout = logging.StreamHandler(sys.stdout) # set a format which is simpler for console use formatter = logging.Formatter('%(levelname)-6s %(message)s') # tell the handler to use this format stdout.setFormatter(formatter) logSys.addHandler(stdout) # Set the configuration path self.__configurator.setBaseDir(self.__conf["conf"]) # Set socket path self.__configurator.readEarly() socket = self.__configurator.getEarlyOptions() if self.__conf["socket"] == None: self.__conf["socket"] = socket["socket"] logSys.info("Using socket file " + self.__conf["socket"]) if self.__conf["dump"]: ret = self.__readConfig() self.dumpConfig(self.__stream) return ret # Interactive mode if self.__conf["interactive"]: try: import readline except ImportError: logSys.error("Readline not available") return False try: ret = True if len(args) > 0: ret = self.__processCommand(args) if ret: readline.parse_and_bind("tab: complete") self.dispInteractive() while True: cmd = raw_input(self.PROMPT) if cmd == "exit" or cmd == "quit": # Exit return True if not cmd == "": self.__processCommand(shlex.split(cmd)) except (EOFError, KeyboardInterrupt): print return True # Single command mode else: if len(args) < 1: self.dispUsage() return False return self.__processCommand(args) def __readConfig(self): # Read the configuration self.__configurator.readAll() ret = self.__configurator.getOptions() self.__configurator.convertToProtocol() self.__stream = self.__configurator.getConfigStream() return ret def __readJailConfig(self, jail): self.__configurator.readAll() ret = self.__configurator.getOptions(jail) self.__configurator.convertToProtocol() self.__stream = self.__configurator.getConfigStream() return ret #@staticmethod def dumpConfig(cmd): for c in cmd: print c return True dumpConfig = staticmethod(dumpConfig) class ServerExecutionException(Exception): pass if __name__ == "__main__": client = Fail2banClient() # Exit with correct return value if client.start(sys.argv): sys.exit(0) else: sys.exit(-1)