lunes, 19 de julio de 2010

Iso8583 Tester

En un articulo pasado, publique de manera general como implementar una aplicación que se comunique con el protocolo ISO 8583 VER. En este post les voy a proporcionar una herramienta desarrollada por mi que permite hacer pruebas de protocolo ISO8583 con la librería TRX.

La aplicación la pueden conseguir en CodePlex, permite indicar el servidor y puerto del server ISO8583 contra el cual se van a realizar las pruebas, permite indicar el formato del mensaje (1987 o 1993) y (ASCII o Binario), ademas permite indicar si se va a utilizar 2 o 4 bytes de cabecera del mensaje.

La aplicación permite llenar un grid en donde se coloca el número del campo y el valor de dicho campo, con esto uno podría enviar cualquier mensaje de prueba.

Ver en CodePlex

lunes, 5 de julio de 2010

Exploración de Excel desde .NET

En algunas oportunidades me ha tocado implementar algún tipo de aplicativo que se alimenta de Excel para obtener información, en este articulo voy a exponer los tres métodos que mas utilizo y que he visto que presentan buena estabilidad y desempeño.

Los ejemplos a continuación fueron diseñados para examinar un lote de archivos Excel para buscar información dentro de ellos, la información de cada celda era extraída con el propósito de construir un DataTable que permitiera al usuario de la aplicación ejecutar querys sobre los archivos de Excel usando el comando .Select de los DataTable, la idea era construir un aplicativo que permitiera buscar ciertos patrones en los archivos de Excel. En el caso especifico del proyecto, era para buscar donde había que aplicar conversión monetaria para el Bolivar Fuerte.

La técnica que recomiendo es la tercera que utiliza la librería NExcel, la cual permite extraer las formulas del Excel.


La primera y mas fácil de las técnicas, pero mas limitada es utilizar oldb para hacer querys sobre el Excel como si fuera BD y ahí puedes obtener bastante info


Utilizando una cadena como: Provider=Microsoft.Jet.OLEDB.4.0; Extended Properties="Excel 8.0;HDR=NO;";Data Source=

public static DataTable ObtenerExcelDataTableOleDB(string sConexion)
    {
      DataTable dt = ObtenerDataTable("Excel");
      string[] Hojas = ObtenerExcelSheets(sConexion);
      foreach(string sHoja in Hojas)
      {
        dt = MergeDataTable(dt,ObtenerDataTableHojaExcel(sConexion,sHoja));
      }
      return dt;
    }



private static String[] ObtenerExcelSheets(string sConexion)
{
      OleDbConnection objConn = null;
      System.Data.DataTable dt = null;

      try
      {
        // Create connection object by using the preceding connection string.
        objConn = new OleDbConnection(sConexion);
        // Open connection with the database.
        objConn.Open();
        // Get the data table containg the schema guid.
        dt = objConn.GetOleDbSchemaTable(OleDbSchemaGuid.Tables, null);

        if(dt == null)
        {
          return null;
        }

        String[] excelSheets = new String[dt.Rows.Count];
        int i = 0;

        // Add the sheet name to the string array.
        foreach(DataRow row in dt.Rows)
        {
          excelSheets[i] = row["TABLE_NAME"].ToString();
          i++;
        }
     
        return excelSheets;
      }
      catch(Exception ex)
      {
        throw ex;
      }
      finally
      {
        // Clean up.
        if(objConn != null)
        {
          objConn.Close();
          objConn.Dispose();
        }
        if(dt != null)
        {
          dt.Dispose();
        }
      }
    }


///
/// Obtiene mediate Oledb el contenido de la hoja de un Excel
///
///
Conexión OLEDB
///
La hoja a procesar
/// DataTable con contenido de la hoja
    private static DataTable ObtenerDataTableHojaExcel(string sConexion, string sHoja)
    {
      DataTable dt = ObtenerDataTable("Excel");
      DataSet ds = new DataSet();
      OleDbConnection objConn = new OleDbConnection(sConexion);
      OleDbCommand cmd = new OleDbCommand("SELECT * FROM ["+sHoja+"]",objConn);
      OleDbDataAdapter da= new OleDbDataAdapter(cmd);
      da.Fill(ds);
      int i=1;
      string sArchivo = sConexion.Substring(sConexion.IndexOf("Source=")+7,sConexion.IndexOf(".xls")-(sConexion.IndexOf("Source=")+7));
      foreach(DataRow dr2 in ds.Tables[0].Rows)
      {
        int col = 1;
        string sFecha = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
        foreach(DataColumn dc in ds.Tables[0].Columns)
        {
          if(dr2[col-1].ToString().Length >0)
          {
            DataRow dr = dt.NewRow();
            dr["Fecha"] = sFecha;
            dr["Archivo"] = sArchivo;
            dr["Hoja"] = sHoja.Replace("$","");
            dr["Fila"] = i;
            dr["Columna"] = col;
            dr["Valor"] = dr2[col-1].ToString();
            dt.Rows.Add(dr);
          }
          col++;
        }
        i++;
      }
      return dt;
    }


La segunda alternativa es cargando Excel como un objeto a través de la librería Net.SourceForge.Koogra

  public static DataTable ObtenerExcelDataTable (string sArchivo)
    {
      DataTable dt = ObtenerDataTable(sArchivo);
      Net.SourceForge.Koogra.Excel.Workbook wb = new Net.SourceForge.Koogra.Excel.Workbook(sArchivo);
      foreach(Worksheet ws in wb.Sheets)
      {
        for(int r = ws.Rows.FirstRow; r <= ws.Rows.LastRow; ++r)
        {
          Net.SourceForge.Koogra.Excel.Row row = ws.Rows[(ushort)r];
          if(row != null)
          {
            for(int c = row.Cells.FirstCol; c <= row.Cells.LastCol; ++c)
            {
              Net.SourceForge.Koogra.Excel.Cell cell = row.Cells[(byte)c];
              if(cell != null)
              {
                DataRow dr = dt.NewRow();
                dr["Fecha"] = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
                dr["Archivo"] = sArchivo;
                dr["Hoja"] = ws.Name;
                dr["Fila"] = r + 1;
                dr["Columna"] = c + 1;
                dr["Valor"] = cell.Value;
                dt.Rows.Add(dr);
              }
            }
          }
        }
      }
      return dt;
    }

///
/// Permite obtener un DataTable a partir de Un archivo de Excel con Kogra
///
///
Archivo a consultar
/// DataTable con toda la data del Excel
    public static DataTable ObtenerExcelDataTable (string sArchivo)
    {
      DataTable dt = ObtenerDataTable(sArchivo);
      Net.SourceForge.Koogra.Excel.Workbook wb = new Net.SourceForge.Koogra.Excel.Workbook(sArchivo);
      foreach(Worksheet ws in wb.Sheets)
      {
        for(int r = ws.Rows.FirstRow; r <= ws.Rows.LastRow; ++r)
        {
          Net.SourceForge.Koogra.Excel.Row row = ws.Rows[(ushort)r];
          if(row != null)
          {
            for(int c = row.Cells.FirstCol; c <= row.Cells.LastCol; ++c)
            {
              Net.SourceForge.Koogra.Excel.Cell cell = row.Cells[(byte)c];
              if(cell != null)
              {
                DataRow dr = dt.NewRow();
                dr["Fecha"] = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
                dr["Archivo"] = sArchivo;
                dr["Hoja"] = ws.Name;
                dr["Fila"] = r + 1;
                dr["Columna"] = c + 1;
                dr["Valor"] = cell.Value;
                dt.Rows.Add(dr);
              }
            }
          }
        }
      }
      return dt;
    }


//Tercer método y el mas recomendado sobre todo si desean obtener información de las formulas



    ///
    /// Permite obtener un DataTable a partir de Un archivo de Excel con NExcel
    ///
    ///
Archivo a consultar
    /// DataTable con toda la data del Excel
    ///
Indica si se deben incluir formulas o no
    public static object[] ObtenerExcelDataTable (string sArchivo,bool bFormulas, bool bNoGuardarData)
    {
      object[] Resultado = new object[4];
      DataTable dt = ObtenerDataTable(sArchivo);
      NExcel.Workbook wb = NExcel.Workbook.getWorkbook(sArchivo);
      int iNumHojas = 0;
      int iNumFormulas = 0;
      int iCeldasData = 0;

      foreach(NExcel.Sheet ws in wb.Sheets)
      {
        iNumHojas++;
        for (int r = 0; r < ws.Rows; r++)
        {
          for(int c=0; c
          {
            NExcel.Cell cell = null;
            try
            {
              cell = ws.getCell(c,r);
            }
            catch(Exception ex)
            {
              if(!bNoGuardarData)
              {
                DataRow dr = dt.NewRow();
                dr["Fecha"] = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
                dr["Archivo"] = sArchivo;
                dr["Hoja"] = ws.Name;
                dr["Fila"] = r + 1;
                dr["Columna"] = c + 1;
                dr["Valor"] = "CEODiscovery:Error obteniendo Celda."+ex.Message;
                dt.Rows.Add(dr);
              }
            }
            if(cell != null && cell.Contents != null)
            {
              if(cell.Contents.Length >0)
              {
                if(!bNoGuardarData)
                {
                  DataRow dr = dt.NewRow();
                  dr["Fecha"] = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
                  dr["Archivo"] = sArchivo;
                  dr["Hoja"] = ws.Name;
                  dr["Fila"] = r + 1;
                  dr["Columna"] = c + 1;
                  dr["Valor"] = cell.Contents;
                  dt.Rows.Add(dr);
                }
                  iCeldasData++;
              }
              if(bFormulas && cell.Type.ToString()=="Numerical Formula")
              {
                if(!bNoGuardarData)
                {
                  string sFormula = "";
                  try
                  {
                    string sTipo = cell.GetType().ToString();
                    switch(sTipo)
                    {
                      case "NExcel.Read.Biff.NumberFormulaRecord":
                        NExcel.Read.Biff.NumberFormulaRecord  form = ((NExcel.Read.Biff.NumberFormulaRecord)(cell));
                        sFormula = ":" + form.Formula;
                        break;
                      case "NExcel.Read.Biff.SharedNumberFormulaRecord":
                        NExcel.Read.Biff.SharedNumberFormulaRecord  form2 = ((NExcel.Read.Biff.SharedNumberFormulaRecord)(cell));
                        sFormula = "2:" + form2.Formula;
                        break;
                      default:
                        sFormula = " Formato de formula no procesado"; 
                        break;
                    }
                  }
                  catch(Exception ex)
                  {
                  sFormula = "Error al leer formula:" + ex.Message;
                  }
                  DataRow dr = dt.NewRow();
                  dr["Fecha"] = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
                  dr["Archivo"] = sArchivo;
                  dr["Hoja"] = ws.Name;
                  dr["Fila"] = r + 1;
                  dr["Columna"] = c + 1;
                  dr["Valor"] = "Formula" + sFormula;;
                  dt.Rows.Add(dr);
                }
                iNumFormulas++;
              }
            }
          }
        }
      }
      Resultado[0] = dt;
      Resultado[1] = iNumHojas;
      Resultado[2] = iNumFormulas;
      Resultado[3] = iCeldasData;
      return Resultado;
    }



Un tema interesante del proyecto es que se debía buscar info no solo en las celdas normales de Excel sino que también en las Macros, para lo que tuve que utilizar otra aplicación que extrajera las macros, algo como:

//Este método llama a mi otra aplicación que genera un txt de las macros, no se incluyó el código dentro del //mismo proyecto debido a que no se deseaba atar el proyecto principal de búsqueda a las librerías de Excel
//que son necesarias para poder sacar las macros.
private long[] ProcesarMacros(string sArchivo)
    {
      long[] Respuesta = new long[2];
      Respuesta[0] = 0;
      Respuesta[1] = 0;
      try
      {
        Process p = new Process();
        p.StartInfo.WorkingDirectory = Path.GetDirectoryName(System.Windows.Forms.Application.ExecutablePath)+"\\" + ConfigurationSettings.AppSettings["VersionOffice"].ToString();
        p.StartInfo.FileName = ConfigurationSettings.AppSettings["AplicacionMacros"].ToString();
        string sFile = Path.GetDirectoryName(System.Windows.Forms.Application.ExecutablePath)+"\\Macro"+Path.GetFileName(sArchivo)+".txt";
        p.StartInfo.Arguments = "\"" + sArchivo + "\" \""+ sFile +"\"";
        p.StartInfo.CreateNoWindow = false;
        p.Start();
        p.WaitForExit();
        if(File.Exists(sFile))
        {
          object[] Obj = Utilidades.ObtenerArchivoDataTable(sFile,"VB",cbNoGuardarData.Checked);
          DataTable dtArchivo = (DataTable)Obj[0];
          dtResul = Utilidades.MergeDataTable(dtResul,dtArchivo);
          FileInfo fi = new FileInfo(sFile);
          Respuesta[0] = fi.Length;
          Respuesta[1] = (int)Obj[1];
          fi.Delete();
        }
        if(File.Exists(Path.GetDirectoryName(System.Windows.Forms.Application.ExecutablePath)+"\\ErrorEnMacro.txt"))
        {
          using (StreamReader sr = File.OpenText(Path.GetDirectoryName(System.Windows.Forms.Application.ExecutablePath)+"\\ErrorEnMacro.txt")) 
          {
            string sError = sr.ReadToEnd();
            MessageBox.Show("Error procesando Macro:"+sError);
          }
          File.Delete(Path.GetDirectoryName(System.Windows.Forms.Application.ExecutablePath)+"\\ErrorEnMacro.txt");
        }
      }
      catch(Exception ex)
      {
        MessageBox.Show("Macros Error:"+ex.Message);
      }
      return Respuesta;
    }

//Este es el código que saca la info de las macros

///
/// Programa que extrae las macros de un Excel
///
class ExtractExcel
{
///
/// The main entry point for the application. El primer parametro de args debe ser el Excel a procesar
/// El segundo parametro de args debe ser el nombre del archivo a donde se quiere  generar el código.
///
    [STAThread]
    static void Main(string[] args)
    {
      //args = new string[2];
      //args[0] = "c:\\AppAttrib4MS.xls";
      //args[1] = "c:\\MacrosExcel.txt";
      
      StreamWriter sw = null;
      
        Excel.Application objXLApp = null;
        Excel.Workbook  objXLABC = null;
        Excel.Workbooks  objXLWorkbooks = null;
      try
      {
        sw = new StreamWriter(args[1]);
        VBIDE.VBProject objProject;
        VBIDE.CodeModule objCode;
        object oMissing = System.Reflection.Missing.Value;

        int iLine=0;
        string sProcName;

        System.Globalization.CultureInfo oldCI  = System.Threading.Thread.CurrentThread.CurrentCulture;
        System.Threading.Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo("en-US");

        objXLApp = new Excel.Application();
        objXLApp.Visible = false;
        objXLWorkbooks = objXLApp.Workbooks;    
        objXLABC = objXLWorkbooks.Open(args[0].ToString(),oMissing,oMissing,oMissing,oMissing,oMissing,oMissing,oMissing,oMissing,oMissing,oMissing,oMissing,oMissing,oMissing,oMissing);     

        VBIDE.vbext_ProcKind pk;
        objProject = objXLABC.VBProject;
        foreach(VBIDE.VBComponent objComponent in objProject.VBComponents)
        {
          try
          {
            objCode = objComponent.CodeModule;
            iLine = 1;
            while (iLine < objCode.CountOfLines)
            {
              sProcName = objCode.get_ProcOfLine(iLine,out pk);
              if(sProcName !=null && sProcName.Length >0)
              {
                iLine = iLine + objCode.get_ProcCountLines(sProcName, pk);
                sw.WriteLine(objCode.get_Lines(1,iLine));
                sw.Flush();
              }
              else
              {
                iLine = iLine + 1;
              }
            }
          }
          catch{}
        }
        sw.Close();
        objXLABC.Close(false,null,null);
        objXLApp.Quit();
      }
      catch(Exception ex)
      {
        StreamWriter sw2 = new StreamWriter(Path.GetDirectoryName(args[1])+"\\ErrorEnMacro.txt");
        sw2.WriteLine("Error:"+ex.Message+" Trace:"+ex.StackTrace);
        sw2.Close();
        if(sw!=null)
        {
          sw.Close();
        }
        if(objXLABC != null)
          objXLABC.Close(null,null,null);
        if(objXLApp != null)
          objXLApp.Quit();
      }
    }  
}


Si usted necesita una solución profesional de como operar archivos Excel desde .NET, no dude en contactarme para conversar y ver como podemos ayudarlo.

domingo, 13 de junio de 2010

Mensajería ISO 8583 para la Banca

Uno de los proyectos mas importantes que he participado es el desarrollo de un internet banking, en el cual el corazón del mismo
se basa en la comunicación con el core Bancario utilizando el estandar ISO 8583

En la siguiente dirección se puede ver un poco de teoría de la definición de ISO 8583: http://es.wikipedia.org/wiki/ISO_8583

Para implementar el proyecto nos basamos en la librería http://sourceforge.net/projects/trx-framework/ la cual es muy buena para el tema de armar los mensajes ISO8583

Crear los mensajes y procesar las respuestas es muy facil, lo interesante en este proyecto fue el manejo de los puertos de comunicación con el core.

Basicamente para mandar un mensaje hay que:

Iso8583Message MensajeEnviar = new Iso8583Message(2000);
MensajeEnviar.Fields.Add(32, 310000);
MensajeEnviar.Fields.Add(2, sNumTarjetaDebito);
Comun Sender = new Comun();
RespuestaDTO = Sender.EnviarMensaje(MensajeEnviar);
//Si la respuesta es exitosa vamos a revisar el mensaje
if (RespuestaDTO.bExitoso)
                {
//Se verifica el campo 11 que es el utilizado como identificador unico del mensaje
                    if (RespuestaDTO.MensajeRecibido.Fields[TipoCampo.Field11Trace] != null)
//Revisamos el campo 39 que contiene el código de respuesta del mensaje
                        switch (int.Parse(RespuestaDTO.MensajeRecibido.Fields[TipoCampo.Field39Respuesta].ToString()))
                        {
//En caso que sea exitoso (00) procesamos el mensaje
//La respuesta del mensaje generalmente viene como un string en uno de los campos, en este caso
//La respuesta es todos la consulta consolidada del cliente dado la cual viene en el campo 48
//En este ejemplo está definido que cada registro de la respuesta es de 126 caracteres con lo cual
//procedemos a construir un array con cada uno de los registros del campo 48 y de ahí sacamos la data
                            case CodigoRespuesta.RespuestaExitosa:
                                ProductoDTO.bExitoso = true;
                                ModeloObjetos.Banca.Cliente Cliente = new ModeloObjetos.Banca.Cliente();
                                string sProducto = RespuestaDTO.MensajeRecibido.Fields[TipoCampo.Field48Productos].ToString();

                                //Se crea el array de de registros (a través de un metodo utilitario)
Array sRegistros = Comun.ProcesarResultado(RespuestaDTO.MensajeRecibido.Fields[TipoCampo.Field48Productos].ToString(), 126, 0);
//se recorren cada uno de los registros

                                foreach (string sRegistro in sRegistros)
                                {
                                    ModeloObjetos.Banca.Producto Producto = new ModeloObjetos.Banca.Producto();
//Extraemos la data de cada campo de acuerdo a lo especificado por el core
                                    Producto.sNumeroProducto = sRegistro.Substring(0, 20).Trim();
                                    Producto.sCodProducto = sRegistro.Substring(20, 6).Trim();
                                    Producto.sProductoNombre = sRegistro.Substring(26, 40).Trim();
                                    Producto.enumTipoProducto = Comun.ObtenerTipoProducto(Producto.sCodProducto);
                                    Producto.dSaldo1 = double.Parse(Comun.FormatearMontoIso(sRegistro.Substring(66, 12)));
                                    Producto.dSaldo2 = double.Parse(Comun.FormatearMontoIso(sRegistro.Substring(78, 12)));
                                    Producto.dSaldo3 = double.Parse(Comun.FormatearMontoIso(sRegistro.Substring(90, 12)));
                                    if (Producto.enumTipoProducto == enumTipoProducto.DPF)
                                    {
                                        Producto.dtFecha = Comun.FormatearFechaStringCorta(sRegistro.Substring(106, 8));
                                        Producto.dSaldo4 = double.Parse(Comun.FormatearMontoIso(sRegistro.Substring(114, 12)));
                                    }
                                    else if (Producto.enumTipoProducto == enumTipoProducto.TDC)
                                    {
                                        Producto.dSaldo4 = double.Parse(Comun.FormatearMontoIso(sRegistro.Substring(102, 12)));
                                        Producto.dtFecha = Comun.FormatearFechaStringCorta(sRegistro.Substring(118, 8));
                                    }
                                    else
                                    {
                                        Producto.dSaldo4 = double.Parse(Comun.FormatearMontoIso(sRegistro.Substring(102, 12)));
                                    }
                                    ProductoDTO.Items.Add(Producto);
                                }
                                ProductoDTO.Items.Sort(ProductoDTO);
                                break;
                            default:
                                ProductoDTO.bExitoso = false;
                                ProductoDTO.sMensaje = RespuestaDTO.sMensaje;
                                break;
                        }
                    else
                    {
                        ProductoDTO.bExitoso = false;
                        ProductoDTO.sMensaje = "Error campo de respuesta (39) nulo";
                    }
                }
                else
                {
                    // El mensaje ha expirado
                    ProductoDTO.bExitoso = false;
                    ProductoDTO.sMensaje = "MensajeExpiro";
                }


Como comenté antes, lo interesante es el manejo de los puertos, que aunque en realidad es sencillo, tuvimos que darle muchas vueltas para asegurar la confiabilidad y estabilidad del servicio

La solución implementada se basa en utilizar un pool de puertos para comunicarnos con el core, este pool fue implementado como una cola:

static Queue CanalesComunicacion = new Queue();

De está cola, antes de enviar un mensaje el código agarra un puerto, verifica que esté bueno, se envía el mensaje y se devuelve el puerto a la cola.
Si el mensaje falla o expira, se considera que el puerto está malo y por ende se agrega el mismo a otra cola de puertos en cuarentena.

Cada cierto tiempo se intenta mandar un mensaje ISO 8583 de prueba para los puertos en cuarenta, si el mensaje es exitoso, se saca el puerto de cuarentena y se devuelve el mismo a la cola de puertos disponibles para los mensajes

        ///
        /// Metodo encargado del envío de mensasjes ISO8583
        ///
        ///
Mensaje ISO 8583 a ser enviado
        /// DTO con mensaje enviado y recibido asi como la info sobre el envío
        public ModeloObjetos.DTO.MensajeDTO EnviarMensaje(Iso8583Message Mensaje)
        {
            int campoConsecutivo = 11;
//se agrega el campo 11 que sirve como identificador unico del mensaje (es obligatorio)
            Mensaje.Fields.Add(campoConsecutivo, NuevoNumeroSecuencia.ToString());
            ModeloObjetos.DTO.MensajeDTO RespuestaDTO = new ModeloObjetos.DTO.MensajeDTO();
            Trx.Messaging.Iso8583.Iso8583Message Respuesta = null;
            int iNumIntentos = 0;
            ClientPeer Canal = null;
            int canalesDisponibles;
//se asegura la exclusividad mientras se revisa si hay puertos disponibles para el envío del mensaje
            lock (SyncRoot)
            {
                while (CanalesComunicacion.Count == 0 && iNumIntentos < _NumeroIntentosEsperaCanalISO)
                {
                    System.Threading.Thread.Sleep(_TiempoEsperaCanalDisponibleISO);
                    iNumIntentos++;
                }
                if ((canalesDisponibles = CanalesComunicacion.Count) > 0)
                {
                    iNumIntentos = 0;
                    while (iNumIntentos < _NumeroIntentosEsperaCanalISO)
                    {
                        for (int i = 0; i < canalesDisponibles; i++)
                        {
//al tener el canal disponible lo saco de la cola para usarlo
                            Canal = CanalesComunicacion.Dequeue();
                            if (Canal.IsConnected)
                            {
                                break;
                            }
                            else
                            {
                                CanalesComunicacion.Enqueue(Canal);
                            }
                        }
                        if (Canal.IsConnected)
                        {
                            break;
                        }
                        System.Threading.Thread.Sleep((int)System.Math.Pow(2, iNumIntentos) * 1000);
                        iNumIntentos++;
                    }
                }
            }
            bool canalQuebrado = false;
//cuando se determina que hay un canal disponible nos disponemos a usarlo
            if (canalesDisponibles > 0)
            {
                try
                {

                    if (Canal.IsConnected)
                    {
                        RespuestaDTO.Trazas.Add(new ModeloObjetos.DTO.TrazaOperacionDTO(false, "MensajeAenviar:" + Mensaje.ToString()));
//Asigno el canal al request a enviar
                        PeerRequest request = new PeerRequest(Canal, Mensaje);
//Se envía el mensaje
                        request.Send();
//esperamos por la respuesta del mensaje ( de acuerdo al timeout establecido)
                        request.WaitResponse(_TimeoutRecepcionMensajeISO);
//Si el request no expiró, entonces está todo bien y guardamos la respuesta ISO en nuestro DTO de respuesta
                        if (!request.Expired)
                        {
                            if (ServiciosISOServer.Comun.Logger.IsDebugEnabled)
                                ServiciosISOServer.Comun.Logger.Debug("Respuesta: " + request.ResponseMessage.ReceivedData);
                            Respuesta = (Iso8583Message)request.ResponseMessage;
                            RespuestaDTO.bExitoso = true;
                            RespuestaDTO.Trazas.Add(new ModeloObjetos.DTO.TrazaOperacionDTO(false, "RespuestaMensaje:" + Respuesta.ToString()));
                        }
                        else
                        {
//Mandamos un correo avisando que el puerto está malo (para que la gente del core lo revise)
                            RespuestaDTO.Trazas.Add(new ModeloObjetos.DTO.TrazaOperacionDTO(false, "MensajeExpiro"));
                            string puerto = ((TcpChannel)Canal.Channel).Port.ToString();
                            string correo = ConfigurationManager.AppSettings["CorreoOperador"];
                            string sText = String.Format("Expiró un mensaje enviado al Servidor {0} Puerto: {1}", ((TcpChannel)Canal.Channel).HostName, puerto);
                            string sSubject = String.Format(ConfigurationManager.AppSettings["Subject"], puerto);
                            string sResultado = EnviarCorreo.EnvioCorreo(correo, sSubject, sText);
                            if (ServiciosISOServer.Comun.Logger.IsDebugEnabled)
                                ServiciosISOServer.Comun.Logger.Debug(sText + " enviado a " + correo + " Resultado: " + sResultado);
                            canalQuebrado = true;


                        }
                        //if(request.ResponseMessage != null)
                        //    RespuestaDTO.Trazas.Add(new ModeloObjetos.DTO.TrazaOperacionDTO( false, "DataRecibida:" + request.ResponseMessage.ReceivedData));
                    }
                    else
                    {
                        RespuestaDTO.Trazas.Add(new ModeloObjetos.DTO.TrazaOperacionDTO(false, "CanalNoConectado"));
                    }
                }
                finally
                {
                    lock (SyncRoot)
                    {
//Si el canal fue utilizado satisfactoriamente, lo agregamos de nuevo a la cola de puertos disponibles para envío de mensajes
                        if (canalQuebrado == false)
                        {
                            CanalesComunicacion.Enqueue(Canal);
                        }
                        else
                        {

                            if (ServiciosISOServer.Comun.Logger.IsDebugEnabled)
                            {
                                ServiciosISOServer.Comun.Logger.DebugFormat("El canal {0} ha sido movido a la cola de cuarentena", Canal.Name);
                            }
//Si el mensaje expiró, agregamos el puerto a la cola de cuarentena (para que no sea nuevamente usado para enviar mensajes de la app hasta que esté sano)
                            MonitorCuarentena.Current.AgregarObjetoCuarentena(Canal);
                        }
                    }
                }
            }
            else
            {
                RespuestaDTO.Trazas.Add(new ModeloObjetos.DTO.TrazaOperacionDTO(false, "CanalesNoDisponibles"));
            }
            RespuestaDTO.MensajeEnviado = Mensaje;
            RespuestaDTO.MensajeRecibido = (Respuesta != null ? Respuesta : new Iso8583Message());
            if (ServiciosISOServer.Comun.Logger.IsDebugEnabled)
                ServiciosISOServer.Comun.Logger.Debug("Retornando: " + RespuestaDTO.bExitoso);
            return RespuestaDTO;

        }


Otra cosa a tener en cuenta es que seguro tendrá que hacer cambios en la especificación del mensaje ISO8583 en la clase: Iso8583Ascii1987MessageFormatter, en esta clase
están definidos como debe ser cada uno de los campos de los mensajes ISO por ejemplo:

FieldFormatters.Add( new StringFieldFormatter(
                48, new VariableLengthManager(0, 9999, StringLengthEncoder.GetInstance(9999)),
                StringEncoder.GetInstance(), "Additional data - private" ) );

Aquí se indica que el campo 48 es tipo String de longitud variable y en este caso con una longitud maxima de 9999 caracteres

En este otro ejemplo el campo 42 es string pero con longitud fija de 15
 FieldFormatters.Add( new StringFieldFormatter(
                42, new FixedLengthManager( 15 ), StringEncoder.GetInstance(),
                "Card acceptor identification code" ) );

Estas definiciones son muy importantes ya que son las primeras que arrojan errores en las pruebas ya que cada server implementa a veces el estandar a su manera

Si está interesado en consultoría en comunicación ISO 8583 o necesita implementar una solución de este tipo, por favor no dude en contactarme.
Luego publicaré una aplicación de pruebas llamada ISO Tester que sirve para hacer pruebas puntuales de este tipo de mensajería

domingo, 30 de mayo de 2010

Comunicación SFTP (SSH)

Hace poco tuve que modificar un componente que se comunicaba con FTP para bajar unos archivos de unas centrales Avaya, la modificación consistió en que la gente de Avaya cambió el protocolo de FTP a SFTP y resulta que no hay muchas opciones opensource para que este cambio sea transparente ya que el protocolo cambia significativamente (aunque los comandos no)

Para aclarar conceptos de FTP vs FTPS vs SFTP pueden consultar: http://www.compute-rs.com/es/consejos-781932.htm
 Una breve definición en ingles:  http://www.delphi3000.com/articles/article_4881.asp
* FTP — File Transfer Protocol. Popular and fast way of moving files between a client and a server. The problem with FTP is that it’s not secured by encryption, leaving files at risk of being compromised during transport.
* FTPS — File Transfer Protocol over SSL. FTPS is an encrypted flavor of the FTP protocol (kind of like how HTTPS is an encrypted flavor of HTTP).
* SFTP — SSH File Transfer Protocol. SFTP uses the Secure Shell (ie: SSH) protocol to encrypt all file transfer communications. SFTP is a bit more firewall friendly because it uses only 1 port and it’s also a bit more secure than FTPS. SFTP is gaining steam as the most preferred method of secure file transfer, particularly in infrastructures that favor unix but SFTP is quickly gaining steam in Windows environments as well.

Luego de mucho buscar en Internet me conseguí que existen muchos componente que permiten hacer los llamados SFTP, el problema es que casi todos eran pagos, solo conseguí uno OpenSource pero no me funcionó (SharpSSH) por lo que tuve que implementar otro tipo de solución.

La solución que me funcionó perfectamente no utiliza un componente para los llamados SSH, sino que utiliza las capacidades de scripting de una aplicación OpenSource usada como cliente SFTP llamada WinSCP.

A continuación el core de las llamadas a la aplicación WinSCP para poder hacer comandos SFTP

public static string  EjecutarComandoSFTP(string logname, string Comando, string IP, string Usuario, string Password, string Host, string Port, string HostKey)
{
/// Run hidden WinSCP process
Process winscp = new Process();
//ruta del programa WinSCP.com
winscp.StartInfo.FileName = ConfigurationSettings.AppSettings["rutaWinSCP"];
//se guarda el archivo de log con el resultado de la operación
logname = logname.Replace(".xml"," " + Comando.Substring(0,Comando.IndexOf(" ") + 1) + ".xml");
winscp.StartInfo.Arguments = "/log=\"" + logname + "\"";
winscp.StartInfo.UseShellExecute = false;
winscp.StartInfo.RedirectStandardInput = true;
winscp.StartInfo.RedirectStandardOutput = true;
winscp.StartInfo.CreateNoWindow = true;
winscp.Start();
/// Feed in the scripting commands
winscp.StandardInput.WriteLine("option batch abort");
winscp.StandardInput.WriteLine("option confirm off");
//Comando de conexión, pendiente con el hsotkey o finger print
string ComandoFTP = "open sftp://" + Usuario + ":" + Password + "@" + Host + ":" + Port + " -hostkey=\"" + HostKey + "\"";
winscp.StandardInput.WriteLine(ComandoFTP);
winscp.StandardInput.WriteLine(Comando);
winscp.StandardInput.Close();

/// resultado de la ejecución del comando
string output = winscp.StandardOutput.ReadToEnd();
Log("Resultado de EjecutarComandoSFTP : " + output);
/// Wait until WinSCP finishes
winscp.WaitForExit();
return logname;
}

Básicamente lo que hace el código en cada llamada a EjecutarComandoSFTP es ejecutar el programa WinSCP.com pasando los parametros de conexión al servidor SFTP y pasando el comando a ejecutar, el resultado de la ejecución es guardado en un archivo XML creado por WinSCP.com el cual detalla los resultados del comando enviado.

Ejemplo de llamada para ejecutar un LS (un listado del directorio activo)

logname = EjecutarComandoSFTP(lognameBase,"LS " + DirectorioRemoto ,IP,Usuario,Password,IP,Puerto,HostKey);

Es muy importante manejar el nombre del Log para cada comando ya que necesitan de ese archivo para poder leer los resultados de la ejecución.

Un tip que hay que tener en consideración para los parámetros de conexión es que hay que pasar el Host Key FingerPrint del servidor al cual nos estamos conectando, si no lo tenemos podemos utilizar a WinScp.exe para obtenerlo o luego del primer intento de conexión, obtener el key del Log donde se registra la falla. un ejemplo de este key es:  ssh-rsa 1024 9b:70:11:10:18:3a:68:ed:66:e4:fd:b6:6c:b3:97:0e

Luego de ejecutar el comando, el resto es simplemente procesar el XML para leer la respuesta, en mi caso yo necesitaba buscar un archivo con un patrón en particular dentro de la ejecución del LS para que si existe dicho archivo, proceder a bajarlo

XPathNodeIterator myXPathNodeIterator2 = (CrearNavegador(sTextoXml2)).Select ("//files/file/filename[starts-with(@value, '"+sNombreArchivoCentralRemoto+"')]");

Utilizando XPATH es muy facil sacar información bien precisa del XML, en el ejemplo anterior obtengo todos los nombres de archivos que comiencen con el patrón dado

Luego hago un ciclo sobre los archivos que coinciden con el patrón y obtengo el nombre real del archivo para ser bajado

while (myXPathNodeIterator2.MoveNext())
{ sNombreArchivoCentralRemoto = BuscaCampoXML(myXPathNodeIterator2,".//@value");

Y ejecuto nuevamente la app WinSCP.com para bajar el archivo, aprovechando para anexar su contenido a uno ya existente en el directorio donde los bajo y ademas eliminando el archivo remoto, todo en un solo comando

logname = EjecutarComandoSFTP(lognameBase,"get " + DirectorioRemoto+ sNombreArchivoCentralRemoto + " " + NombreArchivoLocal + " -append -delete" ,IP,Usuario,Password,IP,Puerto,HostKey);

Luego de eso, vuelvo a leer el archivo XML para ver el resultado de la operación:

string sTextoXml3 = LeerArchivo(System.AppDomain.CurrentDomain.BaseDirectory + logname,"");
if(sTextoXml3 != null && sTextoXml3.Length > 0){
XPathNodeIterator myXPathNodeIterator3 = (CrearNavegador(sTextoXml3)).Select ("//download/result");
string sResultado = BuscaCampoXML(myXPathNodeIterator3,".//@success");
if(sResultado == "true")
Log("Archivo bajado, agregado y eliminado exitosamente");

Como probar este tema? Existe un servidor SSH muy practico y opensource que es ideal para probar, se llama FreeSSH

Si desea implementar una solución de este tipo, por favor contácteme para que coordinemos una reunión para revisar sus requerimientos y como puedo ayudarlo