jueves, 3 de diciembre de 2015

C# - Registrar los Megabytes copiados a extraibles USB

Voy a exponer la lógica y los recursos de código empleados por mí en un programa C# para contabilizar y totalizar los megabyte que se copian en un PC hacia dispositivos extraíbles USB.


Los requerimientos del programa:
  •  Debe detectar todas las unidades de almacenamiento USB que se conectan al pc y contabilizar los megabytes copiados.
  • Debe reportar los megabyte copiados en un archivo de valores separados por coma (csv); un archivo por cada día con al menos la siguiente información: Nombre del volumen, Hora de conexión y Megabytes copiados.
  •  Debe ejecutarse como un proceso sin interface visible.
  • Debe garantizar iniciarse cuando se inicie Windows.

El objetivo de este artículo es publicar mi idea para retroalimentarme con otras ideas.

El núcleo de esta aplicación está sustentado sobre la clase DriverDetector.cs de codeproject.com (http://www.codeproject.com/Articles/18062/Detecting-USB-Drive-Removal-in-a-C-Program). 

Esta clase detecta el mensaje WM_DEVICECHANGE que Windows envía a todas las aplicaciones, este mensajes es enviado por Windows cuando ocurre un cambio en el hardware de la pc; adicionalmente se envía con el mensaje el parámetro WParam que contiene el código especifico del cambio ocurrido. Los valores que interesan de WParam son: 

DBT_DEVICEARRIVAL: es enviado cuando se conecta un dispositivo USB.
DBT_DEVICEQUERYREMOVE: se envía cuando se solicita la extracción del dispositivo.
DBT_DEVICEREMOVECOMPLETE: se envía cuando el dispositivo es removido completamente.

Consecuentemente con esto la clase DriverDetector.cs nos da la posibilidad de escribir código en los procedimientos de eventos:
OnDriveArrived: Código que se ejecutara cuando se conecte un dispositivo USB.
OnQueryRemove: Código a ejecutar antes de remover el dispositivo USB.
OnDriveRemove: Código para cuando se extraiga completamente el dispositivo.

Con estos elementos el trabajo consiste en codificar la siguiente lógica:
  


  • En el evento OnDriveArrived tomar el espacio libre del dispositivo y guardarlo en una variable A (mas adelante veremos que debe ser un arreglo de memoria bidimensional).
  • En el evento OnDriveRemove calcular A-A1 (el resultado es el tamaño de la informacion copiada al dispositivo), escribir el resultado en el archivo csv. (El calculo en este punto no es totalmente cierto; si se borra informacion el resultado del calculo no es exactamente la informacion copiada).

  •         En el evento OnQueryRemove volver a leer el espacio libre del dispositivo y guardarlo en otra variable A1 (debe ser en una columna del arreglo bidimensional).
      La clase DriverDetector.cs es perfecta, me quito el sombrero ante el autor, pero tiene una limitacion que por mas que intente no pude solventar.

     El asunto es el siguiente; para que window envie el submensaje (para llamarle de alguna manera) DBT_DEVICEQUERYREMOVE hay que “decirle explícitamente que lo haga” (esto incluye crear un archivo con ciertas características en la unidad entre otras cosas) de lo contrario no se envía. La clase DriverDetector.cs lo hace, pero solo para un dispositivo a la vez, de manera que no podemos controlar el evento OnQueryRemove para varios dispositivos conectados.

De modo que el punto 2 de la lógica no tenemos como resolverlo. La pregunta es:

¿Qué recurso de software tenemos para resolver esto?

Existe una clase en .NET (namespace System.IO) para monitorear cambios en un directorio y/o subdirectorios y provocar eventos para actuar ante estos cambios; la clase es FileSystemWatcher. Con esta clase se pueden inspeccionar cambios en un directorio tales como creación o eliminación de archivos.

Esta pudiera ser la solución, instanciando la clase para cada dispositivo USB detectados podemos tener el tamaño de los archivos creados (copiados) en la unidad USB. Pero esto tiene otro problema y es que cuando intentemos extraer el dispositivo Windows “nos dira” que el mismo está en uso. Lo lógico es que en el evento que ocurre antes del OnDriveRemove detengamos el monitoreo de la unidad USB por parte de la clase FileSystemWatcher, y ese evento es OnQueryRemove. De modo que estamos en las mismas.

SOLUCION

Mi solución fue desechar la escucha del mensaje DBT_DEVICEQUERYREMOVE y su correspondiente evento OnQueryRemove. En su lugar, cada 1 segundo voy a leer el espacio libre en el dispositivo y lo voy a restar a la lectura anterior, si la diferencia es positiva (mayor que cero) significa que el espacio libre va disminuyendo, por tanto hay en marcha un proceso de copia, entonces voy acumulando la diferencia en una variable (columna de un arreglo de memoria). Finalmente al extraer el dispositivo, el valor de esa variable va a ser el tamaño copiado al dispositivo USB.

Ilustrare esta solución con los “trozos” de código fundamentales.

Namespaces referenciados (independientes de los referenciados por la clase DriverDetector)

using System;
using System.Collections.Generic;
using Microsoft.Win32;
using System.Windows.Forms;
using System.Text;
using System.IO;
using System.Text;

Variables con alcance en todo el formulario

// Variables para almacenar el tamaño del disp., Bay libres y ocupados al conectar
long size0,Bay0,Ocup0;
// Variable de tipo DriveInfo (clase .NET q proporciona acceso a información sobre //una unidad
DriveInfo flash;
String NombreVolumen;
char LetraUnidad;
byte fila_matriz;

//Matrix string 26 filas x 6 columnas, 26 posibles unidades a conectar, 6 columnas
//0-Letra unidad,1-EtiquetaVolumen+Hora de conexión,2-Tamaño, 3-Byte libres al
//conectar,4-Byte Ocupados al conectar,5-Columna de apoyo al calculo,6-Byte copiados
public static string[,] usb_conectada = new string[26, 7];
string Hora_conexion;   

Procedimiento Main de program.cs




     El parámetro “install” si es pasado a la aplicación se crea una llave en el registro para garantizar la carga en cada inicio del sistema, lo contrario con el parámetro “uninstall”

      Evento Load del formulario


        Genarq es una clase del programa con métodos de apoyo a la solución del problema.

         La definición de la clase comienza así:



      Como se ve, incluye la declaración de una variable tIntervalo de tipo Timer (clase .NET para ejecutar métodos a intervalos regulares de tiempo, 1 segundo para nuestro caso).
     
       El método HeadFileCSVRW() y  AddLineFileCSV(string namefile,string linea)

       public static string HeadFileCSVRW()
        {
      string namefile = Application.StartupPath + Path.DirectorySeparatorChar.ToString() + "cusb_" + DateTime.Now.Day.ToString() + "_" + DateTime.Now.Month.ToString() + "_" + DateTime.Now.Year.ToString() + ".csv";
         if (!File.Exists(namefile))
          {
         using (FileStream fs = new FileStream(namefile, FileMode.CreateNew, FileAccess.Write,                FileShare.ReadWrite))
                {
                 using (TextWriter fila_csv = new StreamWriter(fs))
                 {
                 // LetraUnidad,Nombre+HoraConexion,Tamaño,MBL0,MBOcup,BLtmp,BCopiados
                fila_csv.WriteLine("Escrito por: Arquimides R. Ribeaux del Rio");                            fila_csv.WriteLine("Letra_Unidad,Etiqueta_Hora,GZ0,MBL_i,MB_o,MBL_Tmp,MBC");
                fila_csv.Close();
                    }
                }
            }
          return namefile;
         }


    public static void AddLineFileCSV(string namefile,string linea)
    {
  using (FileStream fs = new FileStream(namefile, FileMode.Append, FileAccess.Write, FileShare.ReadWrite))
        {
         using (TextWriter fila_csv = new StreamWriter(fs))
           {
            // LetraUnidad,Nombre+HoraConexion,Tamaño,MBL0,MBOcup,BLBorrado,BLCopiado
             fila_csv.WriteLine(linea);                        
             fila_csv.Close();
            }
         }
     }

     El primer método chequea la existencia del archivo csv, sino existe lo crea con un nombre que incluye la fecha del día y escribe las 2 primeras líneas del fichero y el segundo agrega cada linea al csv despues de extraido el dispositivo

      El procedimiento tProcess1Seg, código del evento Tick del Timer

   public static void tProcess1Seg(Object myobj, EventArgs myEA)
        {
            string lunidad;
            tIntervalo.Stop();      
            for (byte i = 0; i <= 25; i++)
            {
                if (frmMain.usb_conectada[i, 0] != null) //La letra de unidad NO es Null
                {
                  try
                  {
                    //     0 ,1, 2, 3, 4, 5 , 6  
                    // LetraUnidad,Nombre+HoraConexion,Tamaño,BL0,Ocupados,BLtmp,BCopiados
                    lunidad = frmMain.usb_conectada[i,0];
                    DriveInfo unidad = new DriveInfo(lunidad);
                    //BL al iniciar                       
                    long lec_anterior = (long)Convert.ToInt64(frmMain.usb_conectada[i,5]);  
                    //Lectura de espacio libre ahora
                    long lec_now = unidad.AvailableFreeSpace; 
                    // El espacio libre va disminuyendo por tanto estan copiando
                    if ((lec_anterior - lec_now) > 0 ) 
                    {
    long lec_tmp = (long)Convert.ToInt64(frmMain.usb_conectada[i,6])+(lec_anterior - lec_now);
    frmMain.usb_conectada[i,6] = lec_tmp.ToString();                           
                    }
                    frmMain.usb_conectada[i,5]=lec_now.ToString();                                             }
                    catch
                    {
                        tIntervalo.Start();
                        Application.DoEvents();
                        return;
                    }                                      
                }
            }                 
            tIntervalo.Enabled = true;
        }


      Procedimiento para el evento OnDriveArrived

  private void OnDriveArrived(object sender, DriveDetectorEventArgs e)
        {
         // LetraUnidad,Nombre+HoraConexion,Tamaño,BL0,Ocup0,BLBorrado,BLCopiado
         // e.Drive is the drive letter for the device which just arrived, e.g. "E:\\"                 try
            {
              flash = new DriveInfo(e.Drive);
              NombreVolumen = flash.VolumeLabel.ToString(); // obtener el nombre del volumen
              if (NombreVolumen.Length == 0)
               { NombreVolumen = "Sin_Nombre"; }
               // Obtener la primera letra de la cadena, la letra de unidad  
               LetraUnidad = e.Drive[0];   
               size0 = flash.TotalSize;
               Bay0 = flash.AvailableFreeSpace; // Espacio libre inicial
               Ocup0 = size0 - Bay0; // Espacio Ocupado inicial            
             // El ascii de letra unidad-65 para obtener la fila de la matriz           
              fila_matriz = Encoding.ASCII.GetBytes(LetraUnidad.ToString())[0];
             Hora_conexion=DateTime.Now.Hour.ToString()+":"+DateTime.Now.Minute.ToString()+":"DateTime.Now.Second.ToString();
              usb_conectada[fila_matriz - 65, 0] = e.Drive;
              usb_conectada[fila_matriz - 65, 1] = NombreVolumen + "_" + Hora_conexion;
              usb_conectada[fila_matriz - 65, 2] = size0.ToString();
              usb_conectada[fila_matriz - 65, 3] = Bay0.ToString();
              usb_conectada[fila_matriz - 65, 4] = Ocup0.ToString();
              usb_conectada[fila_matriz - 65, 5] = Bay0.ToString();
              usb_conectada[fila_matriz - 65, 6] = "0";
            }
            catch
            {
                Genarq.tIntervalo.Start();
                Application.DoEvents();
                Genarq.tIntervalo.Enabled = true;
                return;
            }

      Procedimiento para el evento OnDriveRemoved

  private void OnDriveRemoved(object sender, DriveDetectorEventArgs e)
        {        
            // Manipular unidad
            LetraUnidad = e.Drive[0];            
            fila_matriz = Encoding.ASCII.GetBytes(LetraUnidad.ToString())[0];
        
            //     0      ,           1       ,   2  , 3 ,  4    ,  5      ,    6   
            // LetraUnidad,Nombre+HoraConexion,Tamaño,BL0,Ocup,BLBorrado,BLCopiado

            long ar2, ar3, ar4, ar5, ar6;            
            ar2 = (long)Convert.ToInt64(usb_conectada[fila_matriz - 65, 2]); // Tamaño
            ar3 = (long)Convert.ToInt64(usb_conectada[fila_matriz - 65, 3]);  //BL al conectar
            ar4 = (long)Convert.ToInt64(usb_conectada[fila_matriz - 65, 4]); 
            ar5 = (long)Convert.ToInt64(usb_conectada[fila_matriz - 65, 5]);  //BL Tmp
            ar6 = (long)Convert.ToInt64(usb_conectada[fila_matriz - 65, 6]);  //B copiados

            string letra = usb_conectada[fila_matriz - 65, 0];
            string nombre = usb_conectada[fila_matriz - 65, 1];

            // Limpiar la fila de la matriz
            usb_conectada[fila_matriz - 65, 0] = null;
            usb_conectada[fila_matriz - 65, 1] = "Sin_Nombre";
            usb_conectada[fila_matriz - 65, 2] = string.Empty;
            usb_conectada[fila_matriz - 65, 3] = string.Empty;
            usb_conectada[fila_matriz - 65, 4] = string.Empty;
            usb_conectada[fila_matriz - 65, 5] = string.Empty;
            usb_conectada[fila_matriz - 65, 6] = string.Empty;
                        
            double Mb2, Mb3, Mb4, Mb5, Mb6;
            Mb2 = ((Convert.ToDouble(ar2) / 1024) / 1024) / 1024; //tamaño
            Mb3 = ((Convert.ToDouble(ar3) / 1024) / 1024); //MBL al conectar
            Mb4 = ((Convert.ToDouble(ar4) / 1024) / 1024); //MB Ocupados al conectar
            Mb5 = ((Convert.ToDouble(ar5) / 1024) / 1024); //BL Temporales
            Mb6 = ((Convert.ToDouble(ar6) / 1024) / 1024); //MB copiados

            string tam = Math.Round(Mb2, 2).ToString() + " GB";
            string mbl0 = Math.Round(Mb3, 2).ToString();
            string mbl1 = Math.Round(Mb4, 2).ToString();
            string mbl2 = Math.Round(Mb5, 2).ToString();
            string mbl3 = Math.Round(Mb6, 2).ToString();            

            string linea = letra + "," + nombre + "," + tam + "," + mbl0 + "," + mbl1 + "," + mbl2 + "," + mbl3;

    // Si pc queda encendida hasta dia sig. el proceso esta cargado de modo q no se           //ejecutar el Form_load
   // Debemos asegurarnos q escriba en el dia correcto

            string nombre_ruta_csv = Genarq.HeadFileCSVRW();

            Genarq.AddLineFileCSV(nombre_ruta_csv, linea);
        }

   
    En el siguiente enlace de Google Drive se puede descargar un fichero compactado (cusb.rar) que contiene el ejecutable cusb.exe.

        https://drive.google.com/file/d/0B1y9zYoMQe86S1djekRtV3RCZ28/view?usp=sharing

      Hasta aquí mi solución.  Al momento de terminar de redactar este artículo, me informan unos amigos (padre e hijo programadores) con los cuales compartí la idea inicial, que lograron controlar el DBT_DEVICEQUERYREMOVE e implementaron la solución utilizando directamente la función API de Windows que esta encapsulada en la clase .NET FileSystemWatcher; desarrollaran en Delphi/Pascal; para ellos el mejor lenguaje de programación que existe.

lunes, 6 de julio de 2015

Script para enviar reportes de acceso de SQUID con SARG



En este artículo voy a compartir mi estrategia en general y un script en particular para enviar por correo a los usuarios de un servidor proxy SQUID sus trazas de navegación a través de la herramienta SARG.

Como siempre hago, no voy a dedicar la entrada a mostrar una guía de instalación de SARG ya que en la red hay en abundancia; siempre prefiero publicar y compartir concretamente las soluciones que he encontrado a los problemas que se me presentan en el día a día en la administración de una red que cuenta con servidores Linux y Windows.
SARG es una aplicación web que permite generar reportes periódicos del log de SQUID, la misma está disponible en los repositorios de DEBIAN y UBUNTU.

apt-get install sarg

Su archivo de configuración se crea en /etc/sarg/sarg.conf 


Como comente más arriba para instalar la aplicación me guie fundamentalmente por el siguiente tutorial 

http://gutl.jovenclub.cu/wiki/doku.php?id=/tutoriales/sarg 


Esta muy buena aplicación también tiene comandos de consola para generar reportes concretos para un periodo y/o para un usuario en particular.

Estrategia General


Mi escenario es el siguiente, tengo un proxy squid corriendo sobre Debian 6.0, los usuarios y contraseña del proxy están almacenados localmente en un archivo de texto generado por la herramienta htpasswd. Se desea que todos los lunes se envié un correo a cada usuario con su traza de navegación de la semana anterior.




Adicionalmente se ha configurado el log de squid para que rote mensualmente 13 veces, o sea que conserve las trazas durante un año de modo que  cada fichero de log comprimido contendrá las trazas de un mes. A continuación mi archivo /etc/logrotate.d/squid

/var/log/squid/access.log {
   compress
   dateext
   rotate 13
   missingok
   postrotate
   test ! -e /var/run/squid.pid || /usr/sbin/squid -k rotate

   endscript

    monthly

  olddir /var/log_squid
  sharedscripts}


Como se puede apreciar los log rotados se almacenaran en la carpeta /var/log_squid que previamente debe ser creada.

Envío de las trazas de navegación a los usuarios

A pesar de que SARG tiene la posibilidad de enviar correo electrónico a través de la interface mail o mailx preferí implementar mi propia solución utilizando los paquetes ssmtp y mpack. A grandes rasgos y quizás no este empleando el lenguaje correcto en el mundo Linux (naci en Windows) ssmtp es un paquete que va instalar una interface mail o mailx en Linux y mpack permite enviar correos con adjuntos desde la línea de comandos.


Debo aclarar que el comando sarg va a generar un reporte de utilización del proxy para un usuario dado, para un intervalo de fecha dado; esto genera una estructura de carpetas con varios archivos html, en mi caso solo me intereso el archivo topsites.html que contiene todos los sitios visitados para el rango de fecha dada.


El script que veremos más adelante en esencia hará lo siguiente:
  
  1. Leer cada usuario de un archivo de texto que contiene la dirección de correo del usuario en cuyo caso el id (parte delante del @) coincide con el id de usuario del proxy.
  2. Generar el reporte para cada usuario.
  3. Enviar por correo al usuario el archivo topsites.html correspondiente

Viendo esto vamos a la esencia.


- Instalamos ssmtp y mpack

apt-get install ssmtp mpack 

Aclarar que ssmtp eliminara postfix o sendmail si estuvieran instalados en el server.

- Configuramos ssmtp

nano /ect/ssmtp/ssmtp.conf  

# Usuario a instancia de quien se enviaran los correos.
root=admin@midominio.cu

# Servidor SMTP de la red
mailhub=192.168.14.3:25


# The full hostname

hostname= midominio.cu



FromLineOverride=YES
#AuthUser=
#AuthPass=


Si tenemos configurada la autentificacion SMTP en el server mail entonces configuramos convenientemente los 2 ultimos parametros del ssmtp.conf

-       Creamos en la carpeta de squid el fichero que contendrá las direcciones de correos de los usuarios, recordar que los id coinciden con los id de usuarios de squid
 
nano /etc/squid/usuarios-proxy-mail.txt  

Contendra algo asi



Como se podrá apreciar más adelante en el script usamos el @ convenientemente como separador de campos; delante de la primera @ está el id de usuario squid y luego con esta y el segundo campo quedaría formada la dirección de correos, el tercer campo es el nombre descriptivo del usuario.

 
El script

-      Creamos un archivo de texto /etc/squid/send_email_nav.sh en la carpeta de squid con el siguiente contenido; recordar que debemos darle derechos de ejecución con la orden:
chmod +x /etc/squid/send_email_nav.sh
 

#!/bin/sh

# Variable directorio padre
dir_padre=/home/rpt-usr-sarg/

# Variables del intervalo de fechas
week=$(date --date "7 days ago" +%d/%m/%Y)
hoy=$(date --date "1 days ago" +%d/%m/%Y)

# Borrar directorios y subdirectorios con datos del anterior reporte
rm -rf $dir_padre

# Crear directorio padre
mkdir $dir_padre

# Tomar usuarios del fichero
users=$(cat /etc/squid/usuarios-proxy-mail.txt)

for u in $users
do

# Tomando usuarios y correos del fichero
id=`echo $u|cut -d@ -f1`
mail=`echo $u|cut -d@ -f2`
nombre=`echo $u|cut -d@ -f3`

# Creando directorios de usuarios
echo "Creando directorio /home/$id -> usuario=$id mail=$id@$mail"
mkdir $dir_padre$id

# Generando reportes
sarg -u $id -o $dir_padre$id -d $week-$hoy

#Moverse a la carpeta del usuaro
cd $dir_padre$id

# Buscar el fichero topsites.html desde la carpeta del usuario
find -type f -name "topsites.html" -exec cp {} $dir_padre$id \;

# Enviar el correo
mpack -s "$nombre le enviamos su traza de navegación del $week-$hoy" $dir_padre$id/topsites.html $id@$mail


# Moverse a la carpeta padre
cd ..

done

# fin del script

-      Finalmente programamos la ejecución del script para todos los lunes a las 7:30 AM (el script contendra las trazas de lunes a domingo de la semana anterior)


Ejecutamos crontab –e

Y escribimos el siguiente contenido

SHELL=/bin/bash

PATH=/sbin:/bin:/usr/sbin:/usr/bin

MAILTO=root

HOME=/



30 7 * * 1 /etc/squid/send_email_nav.sh