lunes, 3 de enero de 2011

Escaneando un servidor FTP

Para hacer una araña que explore un servidor FTP se puede aprovechar el ftplib de python... solo tiene un impedimento, que cuando se pide la lista de archivos en un directorio se devuelve por stdout, lo que es bastante molesto para algo como esto.

La solución más sencilla es substituir la salida estándar ( sys.stdout ) por un objeto que almacene los datos, el único método que hace falta en el objeto es objeto.write(string) , el que se usa para mostrar strings por la pantalla, esto puede servir:


class catcher():
   def clean(self):
       self.trap = ""

   def readlines(self):
       return self.trap.split("\n")

   def readline(self):
       l = self.readlines()
       res = l.pop(0)
       self.trap = "\n".join(l).strip()
       return res
      
   def __init__(self):
       self.clean()

   def write(self, s):
       self.trap += s


Después de dar el cambiazo por stdout y  de que recoja los string, este queda en catcher.trap, además se pueden utilizar los siguientes métodos:

catcher.readlines()
Devuelve lo recojido como un array de strings (una por línea).

catcher.readline()
Devuelve una línea de lo recojido y la elimina.

catcher.clean()
Elimina lo recojido

Después hace falta convertir las líneas en algo útil, esto se puede hacer con:

import sys, re

# Elimina los dobles espacios
def smash(s):
    b = ""
    while b != s:
        b = s
        s = s.replace("  ", " ")
    return s

# Convierte las líneas de `ls` en algo útil
def parseLs(sa, cdir = None):
    d = []
    cd = (cdir != None)

    for s in sa: # Por cada línea
        s = smash(s.strip()) # Se elimina lo que sobra

        if (len(s) < 1): # Si aún queda algo
            continue

        o = {} # Aquí se guardarán los datos
        sld = s.split(" ") # Se separan por los espacios

        # "Si para solucionar un problema se te ocurre
        # 'esto lo puedo hacer con expresiones regulares!'
        # ...ya tienes dos problemas" -- No me acuerdo xD
        if (re.match(".{10}\ [0-9]{1,9}\ [a-z,A-Z,0-9]{1,50}\ " +
            "[a-z,A-Z,0-9]{1,50}\ [0-9]{1,1000}\ [a-z,A-Z,0-9]{3}\ " +
            "[0-9]{2}\ [0-9]{2}\:[0-9]{2}\ .{1,255}", s) != None):

            # Lee la información
            if (len(sld) < 5) or not(":" in s):
                print >>sys.stderr, "-|>", s
                continue

            o['perm' ] = sld[0]
            o['num'  ] = sld[1]
            o['user' ] = sld[2]
            o['group'] = sld[3]
            o['size' ] = sld[4]
            o['mon'  ] = sld[5]
            o['day'  ] = sld[6]
            o['hour' ] = sld[7]

            if (cd):
                o['pwd'] = cdir
              
            n = s.index(":")
            # Obtiene el resto y se guarda como el nombre
            o['name' ] = s[ (s[n : ].index(" ") + n + 1) : ].strip()
            d.append(o) # Se añade a la lista

        # Otra opción de formateado
        elif(re.match(".{10}\ [0-9]{1,9}\ [a-z,A-Z,0-9]{1,50}\ " +
             "[a-z,A-Z,0-9]{1,50}\ [0-9]{1,1000}\ [a-z,A-Z,0-9]{3}\ " +
             "[0-9]{2}\ [0-9]{4}\ .{1,255}",s) != None):

            # Lo mismo
            o['perm' ] = sld[0]
            l = len(sld[0])
            o['num'  ] = sld[1]
            l += len(sld[1])
            o['user' ] = sld[2]
            l += len(sld[2])
            o['group'] = sld[3]
            l += len(sld[3])
            o['size' ] = sld[4]
            l += len(sld[4])
            o['mon'  ] = sld[5]
            l += len(sld[5])
            o['year' ] = sld[6]
            l += len(sld[6])
            o['name' ] = s[ s[ 7 + l : ].index(" ") + 8 + l : ]

            if (cd):
                o['pwd'] = cdir

            d.append(o) # Se añade a la lista
          
        # Si falla
        else:
            print >>sys.stderr, "--->", s, "<---"
            raw_input("")
    return d


Solo hay que alimentar la función parseLs con el array de líneas (desde catcher.readlines() por ejemplo) y opcionalmente con el directorio actual (solo se añade como parámetro) y devuelve un array de diccionarios con al menos las entradas 'perm' (permisos), 'num', 'user', 'group', 'size' y 'mon' (mes).

Nota: parseLs() funciona con los formateados que me he encontrado, que seguramente no sean todos.

A partir de ahí el resto es parecido a una araña normal, por ejemplo [ ftp_crawler.py ], si quieres guardar los datos en un PostgreSQL, hay que descomentar las líneas 8, 41 (aquí esta la query, el nombre de la tabla es nu, cambiala por la que quieras) a 45 y 124 (en esta se define el nombre de la base de datos).
La tabla usa estas columnas: 'permisos', 'numero', 'usuario', 'grupo', 'tamanho', 'path'  (varchar y pista)

Nos vemos

[Referencia]
http://docs.python.org/library/ftplib.html

No hay comentarios:

Publicar un comentario