Clase decoradora para calcular el tiempo de ejecución de funciones

Una de las características de Python es el uso de funciones y clases decoradoras muy útiles cuando estamos evaluando rendimiento y lo que necesitamos es un cronómetro que nos mida cuánto tarda en ejecutarse una función (cualquiera de las que tenemos). Vamos a ver cómo calcular el tiempo de ejecución de funciones:

class getTime:
    def __init__(self, function):
        self.function = function
    def __call__(self, *args, **kwargs):
        start = time.time()
        result = self.function(*args,**kwargs)
        global t_time
        t_time=[self.function.__name__, time.time()-start]
        return result

Esta clase decoradora se puede utilizar de la siguiente forma en todas las funciones para las que queremos medir tiempo:

@getTime
def sendData(data):

¡Espero que os sea útil!


¿Estamos bajo un ataque DoD o DDoS?

Esta tarde he estado leyendo esta entrada (y a la que le “he robado el título”). Como bien comenta el autor, una de las características que un ataque de denegación de servicio (DoS) presenta, ya sea distribuido o no, es el gran número de conexiones que recibirá nuestro sistema. En esta entrada, podéis consultar una serie de órdenes de shell que nos permiten obtener información sobre las conexiones de nuestro servidor y, en función de ésta, detectar un posible ataque.

Algo parecido hace el guión de Python que os copio debajo. Si le introducimos uno de los estados en los puede estar una conexión, nos indica, para cada IP que ha establecido contacto con nuestro servidor, cuantas conexiones están en ese estado (y para cualquier puerto). El guión da para mucho y, evidentemente, también para detectar situaciones anómalas en las que tenemos muchas conexiones “SYN_REC” que denotaría un posible DoS (o DDoS, dependiendo del número de IPs) e, incluso, un posible escaneo de puertos.

#!/usr/bin/python
#encoding:utf-8
try:
 import optparse, sys, re, socket
except:
 print("Error executing 'import optparse, sys, re, socket' command")
 exit(1)
try:
 import socket
 from socket import AF_INET, SOCK_STREAM, SOCK_DGRAM
 import psutil
 from psutil._compat import print_
except:
 print "Error importing socket & psutil module. It is possible you need running 'apt-get install python-psutil'"
 exit(1)
def main():
 parser = optparse.OptionParser("usage%prog " + "-s <state>")
 parser.add_option('-s', dest = 'state', type = 'string', help = 'Please, specify the connection state to analize', default="ESTABLISHED")
 (options, args) = parser.parse_args()
 ipConnected = { }
 for conn in psutil.net_connections(kind='inet'):
 if conn.status == options.state:
 ip=conn.raddr[0]
 if (ip in ipConnected):
 ipConnected[ip]= ipConnected[ip]+1
 else:
 ipConnected[ip]=1
 print "Remote IPtt|# " + options.state + " connections"
 print "---------------------------------"
 for ip in ipConnected.keys():
 print "%stt|%s" % (ip, str(ipConnected[ip]))
if __name__ == '__main__':
 main()

El guión admite muchas mejoras. Por ejemplo, y para el caso comentado, poner un límite a partir del cual nos avise o active un filtro para esa IP. También podríamos comprobar lo mismo, pero solo para un servicio (puerto) determinado y que podríamos introducir como parámetro de entrada.

¡Espero que os sea útil!

 


Como saber las IPs que han realizado más de N peticiones en S segundos a nuestro servicio Apache

Tras una pregunta de un alumno en clase sobre control y detección de peticiones abusivas, recordé el guión que desarrollé, cómo no con Python ;), para detectar “ráfagas” de conexiones. Con Python es muy fácil analizar los logs del servidor Apache y consultar qué IPs nos han realizado más de X peticiones (entendiendo por X un parámetro que introducimos como argumento) en un número de segundos determinado (también introducido como argumento del guión)

Dependiendo de qué información queramos extraer, ajustaremos el número de peticiones y segundos. Por ejemplo, ante subidas en la carga del servidor, podemos comprobar qué IPs cumplen un patrón y, si consideramos que se están realizando con mala fe, bloquear el tráfico proveniente de estas IPs.

Aunque en EPSAlicante rotamos los logs, por si no se hace o por si la información que queremos analizar está en un periodo de tiempo determinado, esto también puede indicarsele al guión (D días, H horas y M minutos antes).

El desarrollo de este guión apenas me llevó 2 horas (y con interrupciones) y, sin embargo creo que es muy útil. Podemos hasta ejecutarlo periódicamente para ver qué obtenemos. ¡Nunca se sabe las sorpresas que nos podemos llevar!

Creo que incluso sería bueno (posible mejora) que la información la proporcionara en XML para integrarlo con otros sistemas de administración y/o seguridad.

Justo debajo de este párrafo está el guión. La función más importante es readHistoryIP que devuelve un diccionario con clave IP y valor otro diccionario cuya clave es la fecha de registro del evento y el valor el número de veces que para esa IP aparece un evento +/- segundos con respecto a la fecha del evento. En el cuerpo principal se muestra la información de las IPs que aparecen más veces que del valor indicado por parámetro para el intervalo de tiempo establecido.

#!/usr/bin/python
#encoding:utf-8
try:
 import sys,optparse, datetime,re
except:
 print("Error running 'import sys,optparse,datetime,re'. Maybe you have to install some python library")

def isValid(ip):
 if (re.match("^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?).){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$",ip)):
 return True
 elif re.match(r'^[a-zA-Z0-9_]{,63}(.[a-zA-Z0-9_]{,63}){3}$',ip): #("^W+(.W)$", ip):
 return True
 else:
 #print "Name/IP host wrong. You have to write a FQDN name or a IP direction"
 return False
 return False

def readHistoryIP(logfile,sec, deltaday,deltahour,deltaminutes):
 months = {"Ene":1, "Feb":2, "Mar":3, "Abr":4, "May":5, "Jun":6, "Jul":7, "Ago":8, "Sept":9, "Oct":10, "Nov":11, "Dic":12}
 today = datetime.datetime.now()
 refDay= datetime.timedelta(days=int(deltaday), hours=int(deltahour), minutes=int(deltaminutes))

 ipHitListing = { }
 try:
 contents = open(logfile, "r")
 except IOError, e:
 print 'Error openning file '+ logfile +": "+e.strerror
 raise
 except:
 print 'Error openning file '+ logfile
 raise
 # go through each line of the logfile
 for line in contents:
 # split the string to isolate the IP address and date
 ip = line.split(" ", 1)[0]
 dateRecord=line.split(" ",5)[4][1:]
 date = dateRecord.split(":",1)[0]
 day=date.split("/",1)[0]
 month=date.split("/",2)[1]
 year =date.split("/",3)[2]
 hour = dateRecord.split(":",2)[1]
 minute = dateRecord.split(":",3)[2]
 seconds = dateRecord.split(":",4)[3]
 eventdate=datetime.datetime(int(year),int(months[month]),int(day),int(hour),int(minute),int(seconds))
 #Check if ip is right
 #print "Analizing IP: '" + ip + "'"
 if isValid(ip):
 if eventdate > today - refDay:
 intervalDelta=datetime.timedelta(seconds=int(sec))
 count=False
 if (ip in ipHitListing): 
 for ipData in ipHitListing[ip].items():
 if eventdate < ipData[0] + intervalDelta:
 ipHitListing[ip]= {ipData[0]:int(ipData[1])+1}
 count=True
 break
 if count == False:
 ipHitListing[ip]= {eventdate:1}
 else:
 ipHitListing[ip]= {eventdate:1}
 return ipHitListing
def main():
 parser = optparse.OptionParser("usage%prog " + "[-f <file>] -n Number of events -s seconds to find events -d number of days to analice -H number of hours to analice -m number of minutes to analice")
 parser.add_option('-f', dest = 'file', type = 'string', help = 'Please, specify the Apache access file', default="/var/log/apache2/access.log")
 parser.add_option('-n', dest = 'number', type = 'string', help = 'Please, specify the number of connections to detect. By default 10', default="10")
 parser.add_option('-d', dest = 'deltaday', type = 'string', help = 'Please, specify the number of days to check. By default 10', default="10")
 parser.add_option('-H', dest = 'deltahour', type = 'string', help = 'Please, specify the number of hours to check. By default 10', default="10")
 parser.add_option('-m', dest = 'deltaminute', type = 'string', help = 'Please, specify the number of minutes to check. By default 10', default="10")
 parser.add_option('-s', dest = 'seconds', type = 'string', help = 'Please, specify the seconds. By default: 1"', default="1")
 (options, args) = parser.parse_args()
 HitsDictionary = readHistoryIP(options.file,options.seconds, options.deltaday, options.deltahour, options.deltaminute)
 for ip in HitsDictionary.keys():
 if HitsDictionary[ip].values()[0] > int(options.number):
 print "Attention---> IP: " + ip + ", " + str(HitsDictionary[ip].values()[0]) + " access in " + options.seconds + " seconds"
if __name__ == "__main__":
 main()

Referencias:

  1. Python para todos, de Raúl González Duque
  2. Python cookbook, de Alex Martelli, Anna Martelli y David Ascher, O’Reilly

El cliente en Python

Y para terminar con el ejemplo de un esqueleto cliente-servidor en Python, el cliente:

#!/usr/bin/python
#encoding:utf-8
try:
 import socket
 import optparse,sys
except:
 print("Error running 'import optparse,socket,sys'. Maybe you have to install some python library :)")
parser = optparse.OptionParser("usage%prog " + "-s <target server> -p <target port>")
parser.add_option('-s', dest = 'server', type = 'string', help = 'Please, specify the target server '-s server'')
parser.add_option('-p', dest = 'port', type = 'string', help = 'Please, specify the target port '-p port'')
(options, args) = parser.parse_args()
if (options.server == None):
 print '[-] You must specify a target server: -s server.'
 exit(0)
if (options.port == None):
 print '[-] You must specify a target port: -p port'
 exit(0)
server = options.server
port = int(options.port)

# Create a TCP/IP socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# Connect the socket to the port on the server given by the caller
server_address = (server, port)
print('connecting to %s port %s' % server_address)
try:
 sock.connect(server_address)
except socket.error , msg:
 print 'Connect failed. Error code: ' + str(msg[0]) + 'Error message: ' + msg[1]
 sys.exit()
try:
 message="Speaking with server "
 print("sending %s" % message)
 sock.sendall(message)
 amount_received = 0
 amount_expected = len(message)
 while amount_received < amount_expected:
 data = sock.recv(16)
 amount_received += len(data)
 print("received %s" % data)
finally:
 sock.close()

Creo que el código se explica solo. Tras realizar una correcta conexión, el cliente intercambiará un mensaje con el servidor. En ese trozo es donde se debería implementar el protocolo de aplicación que necesitemos.

La ejecución del cliente en el servidor implementado en las imágenes

Captura de pantalla de 2014-02-14 18:42:56

Referencias:

  1. Python para todos, de Raúl González Duque
  2. Entrada con la versión sencilla del servidor
  3. Entrada con la versión concurrente del servidor

SD: Esqueleto de servidor concurrente con Python (y2)

Continuando con programas en Python (que para eso es el lenguaje de programación de moda ), veremos un servidor que acepta múltiples peticiones de clientes, cada una de ellas, atendidas por un servicial hijo. Esta constituye la principal mejora que debíamos añadir  al código de servidor básico que vimos en la entrada referenciada.

Aunque sea solo sea un esqueleto de un proceso servidor, para que cumpla un mínimo, debe ser concurrente para que admita más de un cliente. Pensando en estos términos, en seguida nos puede venir a la memoria los Threads, pero con estos Python nos está “engañando” de forma vil. Si consultamos la información sobre implementación y threads en [1], nos daremos cuenta que, al usarlos, lo que Python estará haciendo es que en único proceso irá intercambiando la ejecución de los “hilos” que ha creado para dar la sensación que todo se está ejecutando en paralelo, pero no lo es.

Por esto, para que sea concurrente, con Python he usado procesos y no hilos (aunque podréis encontrar versiones con hilos como esta, esta y esta)

El código:

#!/usr/bin/python
#encoding:utf-8
try:
    import socket,sys,optparse
except:
    print("Error running 'import socket,sys,optparse'. Maybe you have to install some python library")
try:
    import sys,os
except:
    print("Error running 'import  sys,os'. Maybe you have to install some python library")

def comunication(connection, addr):
    print 'Connected with ' + addr[0] + ':' + str(addr[1])
    while True:
        #receive data
        try:
            data = connection.recv(1024)
            #process data
            if not data:
                break
            #elif re.match(data, "QUITn."):
            elif data == "QUITn":
                print 'Received data: ' + data + " from " + addr[0] + ':' + str(addr[1])
                reply = 'BYE'
                connection.send(reply) #send reply
                break
            else:
                print 'Received data: ' + data + " from " + addr[0] + ':' + str(addr[1])
                reply = 'OK...' + data
                connection.send(reply) #send reply
        except KeyboardInterrupt:
            print
            print "Stopped server."
            break
    connection.shutdown(socket.SHUT_RDWR)    
    return

def main():
    parser = optparse.OptionParser("usage%prog " + "-d <ip> -p <target port>")
    parser.add_option('-d', dest = 'ip', type = 'string', help = 'Please, specify the target server')
    parser.add_option('-p', dest = 'port', type = 'string', help = 'Please, specify the target port')
    parser.add_option('-q', dest = 'queue', type = 'string', help = 'Please, specify the queue size')
    parser.add_option('-P', dest = 'process', type = 'string', help = 'Please, specify the maximun number of process')
    (options, args) = parser.parse_args()
    if (options.ip == None):
        print '[-] You must specify a ip direction to listen to.'
        exit(0)
    if (options.port == None):
        print '[-] You must specify a port.'
        exit(0)
    HOST=options.ip
    PORT=int(options.port)
    if (options.queue == None):
        QUEUE=1
    else:
        QUEUE=int(options.queue)
    if (options.process == None):
        PROCESS=2
    else:
        PROCESS=int(options.process)
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  # Create a socket object
    print 'Socket created'
    try:
        s.bind((HOST, PORT)) # Bind to the port
    except socket.error , msg:
        print 'Bind failed. Error code: ' + str(msg[0]) + 'Error message: ' + msg[1]
        sys.exit()
    print 'Socket bind complete'
    s.listen(QUEUE) # Now wait for client connection
    print 'Socket now listening'
    while True:
        try:
            conn, addr = s.accept()
        except KeyboardInterrupt:
            print
            print "Stopped server."
            s.close()
            break
        except socket.error, msg:
            print "Socket error! %s" % msg
            s.close()
            break
        try:
            pid=os.fork()
            if (pid==0): #Child
                comunication(conn, addr)
                conn.close()
                s.close()
                print("Bye child")
                exit(0)
            else: #Father
                print("Made child %s..." % pid)
        except OSError, e:
            sys.stderr.write("Error making process child (fork)")
            break
if __name__ == "__main__":
    main()

Aspectos que debemos tener en cuenta de este esqueleto:

  • En la función comunication(…) es donde pondremos el manejo de la comunicación entre cliente y servidor: es decir, se implementará el protocolo de aplicación que permitirá la conversación entre cliente (para solicitar un recurso/know how) y el servidor.
  • Debemos finalizar educadamente (shutdown)
  • El código que ejecutará el hijo es el comprendido dentro del ‘if PID==0′

En la siguiente imagen se puede comprobar que al recibir 4 peticiones de servicio, el número de procesos serán 5: 4 hijos, uno por cada conexión (y que se encargan de gestionarla) y el padre que está a la espera de más peticiones. Podemos limitar el número máximo de procesos que creamos, si lo consideramos conveniente (se programa y punto 😉 )

Captura de pantalla de 2014-02-03 17:27:42Referencias:

  1. Python para todos, de Raúl González Duque
  2. Entrada con la versión sencilla del servidor

URL to QR code

Para automatizar la generación de códigos QR asociados a determinadas URL, he creado este pequeño guión en Python que, dada un URL y, opcionalmente, un nombre de fichero, guarda en dicho fichero el código QR creado a partir de la URL.
Es muy sencillo, pero me gustaría remarcar la regexp que comprueba la validez de la URL:

  • Los carácteres ‘^‘ y ‘$‘  marcan el inicio y final de la cadena, respectivamente.
  • (https?|ftp)://” indica el protocolo. Se pueden poner muchos más (en esta expresión están http, https y ftp).
  • (www|ftp).)[a-z0-9-]+(.[a-z0-9-]+)+” Nombre y dominio
  • ([/?].*)?” El path y parámetros que pueda haber.

Lo único que no permite es poner el usuario y contraseña. Si alguien lo hace, que la deje modificada en los comentarios 😉

Aquí tenéis el guión:

#!/usr/bin/python
try:
    import qrcode
except:
    print("Error: import qrcode. You have install it: sudo pip install pil qrcode")
import optparse,sys,re
parser = optparse.OptionParser("usage%prog " + "-u <URL> [-s <file to save qr code>]")
parser.add_option('-u', dest = 'url', type = 'string', help = 'Please, specify the url to convert to QR code.')
parser.add_option('-s', dest = 'file', type = 'string')
parser.add_option('-t', dest = 'size', type = 'string')
(options, args) = parser.parse_args()
if (options.url == None):
    print '[-] You must specify a url to convert to QR code.'
    exit(0)
if (re.match("^((https?|ftp)://|(www|ftp).)[a-z0-9-]+(.[a-z0-9-]+)+([/?].*)?$",options.url)):
    url=options.url
else:
    print("%s is not a correct URL" % options.url)
    exit(2)
if (options.file == None):
    if (re.search("http[s]?://", options.url)):
        file="./"+url.split('/')[2]+".png"
    elif (re.search("/", options.url)):
        file="./"+url.split('/')[0]+".png"
    else:
        file="./"+url+".png"
else:
    file=options.file
if (options.size == None):
    size=5
else:
    size=options.size
qr=qrcode.QRCode(version=20, error_correction=qrcode.constants.ERROR_CORRECT_L,box_size=size)
qr.add_data(url)
qr.make
try:
    image=qr.make_image()
    image.save(file)
except OSError, e:
    print 'Error writing QR code file '+e[1]
except:
    print("Error writing QR code file")

Referencias:

  1. https://pypi.python.org/pypi/qrcode
  2. Python para todos, de Raúl González Duque

nmap y python. Un ejemplo

Para realizar pruebas sobre una red o equipo -casi- todos pensamos, en seguida, en nmap. Muchas veces queremos automatizar ciertas tareas y cuando interviene nmap, con el módulo python-nmap, podemos programarlas con este maravilloso lenguaje. Si, por ejemplo, necesitamos saber si un equipo tiene abierto un puerto determinado, si está “vivo”,… podemos fácilmente usar la funcionalidad de nmap e incluirla en nuestro programa sin necesidad de hacer invocaciones de comandos del sistema.

Esta librería nos proporciona varias clases que implementan la funcionalidad, síncrona y asíncrona, de nmap. PortScanner es una de ellas y es la que se utiliza en el sencillo guión que se muestra como ejemplo y que permite comprobar el estado de uno o varios puertos indicados por parámetro. Antes de que se me olvide, en esta página podéis encontrar información.

#!/usr/bin/python
try:
    import optparse, sys, re, socket
except:
    print("Error executing 'import optparse, sys, re, socket' command")
    exit(1)
try:
    import nmap
except:
    print "Error importing nmap module. It is possible you need running 'apt-get install python-nmap'"
    exit(1)
def checkIp(ip):
    if (re.match("^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?).){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$",ip)):
        host = ip
    elif re.match(r'^[a-zA-Z0-9_]{,63}(.[a-zA-Z0-9_]{,63}){3}$',ip): #("^W+(.W)$", ip):
        host = socket.gethostbyname(ip)
    else:
        host=""
        print "Name/IP host wrong. You have to write a FQDN name or a IP direction"
        exit(3)
    return host
def main():
    parser = optparse.OptionParser("usage%prog " + "-H <target host> -p <target port> + -N <target_network>")
    parser.add_option('-H', dest = 'host', type = 'string', help = 'Please, specify the target host')
    parser.add_option('-P', dest = 'protocol', type = 'string', help = 'Please, specify the transport protocol: tcp or udp')
    parser.add_option('-p', dest = 'ports', type = 'string', help = 'Please, specify the target port(s) separated by comma')
    (options, args) = parser.parse_args()
    if (options.host == None): 
        print '[-] You must specify a target host or a target network.'
        exit(0)
    if (options.ports == None): 
        print '[-] You must specify a target port(s).'
        exit(0)
    if ((options.protocol == None) or (options.protocol != "tcp" and options.protocol != "udp")): 
        print '[-] You must specify the transport protocol: tcp or udp.'
        exit(0)
    protocol = options.protocol
    host=checkIp(options.host)    
    ports = options.ports.split(',')
    scanner=nmap.PortScanner() 
    for port in ports:
        try:
            print('Analizing %s, %s/%d' % (host, protocol,(int(port))))
            if (protocol != "tcp"):
                scanner.scan(host, port, '-sU')
            else:
                scanner.scan(host, port)
            print " [+] "+ host + "(" + scanner[host].state() + ") " + protocol +"/" + port + "->" + scanner[host][protocol][int(port)]['state']
        except nmap.PortScannerError:
            print('Nmap ERROR: %s ' % sys.exc_info()[0])
            exit(2)
        except:
            print("Unexpected ERROR: %s" % sys.exc_info()[0])
            exit(3)
        
if __name__ == "__main__":
    main()

Lo que hace el guión es, tras el formateo de los parámetros de entrada:

  1. Se comprueba si con el parámetro -H se ha especificado una dirección IP válida (regexp: “^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?).){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$”),  o un nombre de dominio completamente cualificado (regexp: r’^[a-zA-Z0-9_]{,63}(.[a-zA-Z0-9_]{,63}){3}$’,ip): #(“^W+(.W)$”) y, en ese caso, se resuelve por su ip (socket.gethostbyname(options.host)
  2. Declaramos la clase y se lanza la comprobación, en función del protocolo de transporte. Si es UDP, los permisos deben ser de root (no lo comprueba el guión –> mejora)
  3. Se muestra el resultado para todos los puertos indicados (opción -p), del protocolo definido (-P) y del host deseado (-H).

Las cookies nos permiten ofrecer nuestros servicios. Al utilizar nuestros servicios, aceptas el uso que hacemos de las cookies. Más información.