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