Introducción a las extensiones ISAPI.
Por: Demian Panello.
¿Qué es ISAPI?
ISAPI significa "Internet Server API", o sea, la API para servidores de internet. Recordemos que existe una API, (Application Programming Interface) para diversos productos, por ejemplo, está la API de Windows, la API de DirectX, la API del reproductor multimedia de Windows, etc. Entonces, a la API para los servidores de internet se le suele llamar ISAPI, (son un conjunto de funciones). Se cuenta, ademas, de algunas clases MFC que "envuelven" esas funciones, a saber:
CHttpServer.
CHttpServerContext.
CHtmlStream.
CHttpFilter.
CHttpFilterContext.
Con objetos de estas clases se pueden escribir librerías DLL de extensión de servidor y/o filtro de servidor.
¿Qué puedo hacer con una extension de servidor?.
Una extensión de servidor, (también llamada ISA, Internet Server Application), es una DLL que se carga en el servidor de HTTP y puede ser llamada por el navegador de internet de un usuario como ocurre con los programas CGI, (en realidad es un CGI). Contiene funciones que reciben parámetros desde un formulario de una página de HTML.
Se puede acceder a una base de datos de SQL Server para registrar un usuario, lo que comunmente se hace con ASP o PHP. ¡AH!, pero hacerlo con C++ siempre es más gratificante. ¿No?.
¿Qué puedo hacer con un filtro de servidor?.
Igual que con las extensiones un filtro es una DLL que se carga en un servidor HTTP y se encarga de, precisamente, filtrar la información que va y viene del servidor, (entre el servidor y los clientes).
Una extensión de servidor.
En este articulo escribiremos una extensión de servidor para lo cual habrá que contar con algunos requisitos.
- Internet Information Server, (IIS) preferentemente o bien Personal Web Server en caso de tener Windows 98 o ME.
- Algunos conocimientos de HTML, sin ser esto excluyente.
Vamos a tener una página WEB con dos cuadros de texto y un botón, (un pequeño formulario). El usuario ingresara un nombre de usuario y password, y al pulsar el botón estos datos serán enviádos a nuestra DLL. Una función tomará estos parámetros y buscará ese usuario en un arreglo de usuarios, (objetos usuarios que definiremos), en caso de encontrarlo, comparará el parámetro password con el del usuario y de coincidir se armará una página HTML de respuesta mostrando el ID del usuario y su edad. Cuando no exista el usuario o falte ingresar información, tambien se mostrará algo indicando el error. En este ejemplo no usaremos bases de datos, (lastima ¿no?), sino que el arreglo estará cargado en tiempo de compilación con algunos datos, simulando que ésto fuese una base de datos. En otro artículo veremos como usar SQL Server para realizar ésto último.
Nota: Sólo para satisfacer la curiosidad, le anticipo que para acceder a una base de datos, no podrá usar DAO puesto que no admite multihilo. Puede usar las clase ODBC de MFC, (por ejemplo CDatabase, CRecordset, etc.) o también ADO.
Página HTML de ingreso.
Primero crearemos una página HTML, muy pequeña, para que el usuario se registre.
Puede usar Frontpage o cualquier otro utilitario, pero yo lo invito a que escriba todo el código HTML que sigue en el Notepad ya que le permitirá comprender mejor cada línea.
Escriba el siguiente código HTML y guarde el archivo con el nombre "login.htm" en el directorio virtual de LocalHost, (suele ser c:\Inetpub\wwwroot\).
Atención que el Notepad intentará guardar el archivo con la extensión .txt, para evitar ésto, antes de escribir el nombre del archivo, seleccione en la lista "Tipo", "Todos los archivos".
login.htm
<html> |
Me voy a ocupar de comentar algo con respecto a la línea donde se define el formulario.
Con form action="scripts/dcpejemplo.dll?Login" se le está indicando al formulario cual es la librería de extensión y cual la función a donde "viajarán" los datos de los controles definidos más abajo.
La librería dcpejemplo.dll es la que vamos a crear un par de línea más abajo, luego Login es el nombre de la función que recibirá los datos de esos dos cuadros de texto, Scripts es la carpeta donde se almacenan los programas, (DLL's), que se ejecutarán en el servidor cuya ruta es c:\Inetpub\Scripts allí copiaremos, una vez desarrollada, la librería.
Importante: La función de la librería, especificada en action del formulario, debe esperar tantos parámetros del formulario como controles tenga el mismo, e incluso debe coincidir el tipo de dato. Con esto quiero decir, que si tengo un formulario con dos cuadros de texto, (como nuestro caso de ahora), y en la librería tengo una función, entre otras, que recibe sólo un parámetro de texto y es a ésta a la cual pretendo enviarle los datos del formulario; no piense que descartará el dato sobrante, sino que arrojará un error.
La cláusula Method indica justamente, con que método se enviarán los datos. Nosotros usaremos POST en lgar de GET, y tenga presente que los dos hacen lo mismo, pero GET muestra los parámetros en la barra de dirección del explorador.
Escribiendo la DLL de extensión.
Ingrese a Visual C++ y seleccione proyecto "ISAPI Extension Wizard" y asígnele el nombre "dcpejemplo", como muestra la imágen.
7
En el siguiente paso AppWizard nos ofrece dos tipos de proyectos: Filter Object o Extension Object.
Deje todo como se presenta, pues nuestra intensión es crear una extensión de servidor. Pulse Finish para generar el proyecto.
El AppWizard nos generó la aplicación con una clase llamada CDcpejemploExtension derivada de CHttpServer, pero antes de escribir código en esta clase, paso a explicarle algo.
La clase CHttpServer, junto con otra llamada CHttpServerContext, envuelve la API del servidor de internet y cuando el servidor recibe una petición de un usuario se crea una instancia de ésta y otra de CHttpServerContext. El método miembro HttpExtensionProc lee los datos de los formularios que llegan del usuario y decide que hacer. Otros métodos procesan los datos y arman la respuesta, (otra página de internet).
Nosotros deberemos agregar algún método y sobreescribir otro, pero repasemos qué es lo que queriamos hacer para así entender todo ésto.
La clase CUsuarios.
Tenemos una página WEB con un formulario con dos cuadros de texto, se ingresa un ID de usuario y un password y luego se envían los datos al pulsar el botón "Ingresar". Estos datos deberían llegar al método "Login" de la librería "dcpejemplo.dll". Bien, el proyecto que dará como resultado esta librería lo acabamos de crear, nos falta la función "Login", pero sigamos pensando...
Dije que ésta función se encargaría de buscar ese usuario con ese password en una lista de usuarios. Bueno, paremos aquí; si tenemos una lista de usuarios necesitamos un objeto usuario o sea una clase, asi que ahora incorporaremos una nueva clase al proyecto, la clase CUsuarios.
Pulse Ctrl+W para invocar al Class Wizard, luego pulse Add Class y luego New.
Este es el archivo de definición de la clase CUsuarios, (Usuarios.h).
class CUsuarios |
En el archivo de emplementación, (usuarios.cpp), se desarrolla el constructor, así que agregue lo siguiente:
CUsuarios::CUsuarios(char* strID,
char* strPass, short
int iEdad) |
Ya tenemos nuestra clase CUsuarios, pero para poder declarar un arreglo de usuarios en la clase derivada de CHttpServer, (o sea CDcpejemploExtension), hay que incluir el archivo usuarios.h en el archivo de definición de CDcpejemploExtension, (dcpejemplo.h), esciba:
#include "usuarios.h"
#define MAX_USUARIOS 5
Asi incluimos la clase CUsuarios y además definimos un tope de 5 elementos para el arreglo.
Declaramos, entonces el arreglo, como dato miembro público de la clase CDcpejemploExtension como sigue:
CUsuarios* m_ListaUsuarios[MAX_USUARIOS];
Vamos a usar esa lista como si fuera una base de datos, así que debería estar, ya, cargada; entonces en el constructor de la clase CDcpejemploExtension escribimos:
CDcpejemploExtension::CDcpejemploExtension() { //Aqui voy creando objetos CUsuarios para cada posición del arreglo //y le asigno datos.(ver el destructor de esta clase donde libero la memoria). m_ListaUsuarios[0] = new CUsuarios; strcpy(m_ListaUsuarios[0]->m_strID , "juan"); strcpy(m_ListaUsuarios[0]->m_strPass , "juan123"); m_ListaUsuarios[0]->m_iEdad = 20; m_ListaUsuarios[1] = new CUsuarios; strcpy(m_ListaUsuarios[1]->m_strID , "pedro"); strcpy(m_ListaUsuarios[1]->m_strPass , "pedro123"); m_ListaUsuarios[1]->m_iEdad = 18; m_ListaUsuarios[2] = new CUsuarios; strcpy(m_ListaUsuarios[2]->m_strID , "maria"); strcpy(m_ListaUsuarios[2]->m_strPass , "maria123"); m_ListaUsuarios[2]->m_iEdad = 25; m_ListaUsuarios[3] = new CUsuarios; strcpy(m_ListaUsuarios[3]->m_strID , "demian"); strcpy(m_ListaUsuarios[3]->m_strPass , "demian123"); m_ListaUsuarios[3]->m_iEdad = 29; m_ListaUsuarios[4] = new CUsuarios; strcpy(m_ListaUsuarios[4]->m_strID , "pepe"); strcpy(m_ListaUsuarios[4]->m_strPass , "pepe123"); m_ListaUsuarios[4]->m_iEdad = 36; } |
Como vamos creando objetos CUsuarios en el "monton", (heap), en algún momento debemos eliminarlos para así liberar esa memoria, un buen lugar para hacer eso es el destructor de la clase CDcpEjemploExtension, allí escribiremos:
CDcpejemploExtension::~CDcpejemploExtension() { int i; for(i=0; i<=4;i++) delete m_ListaUsuarios[i]; } |
La función Login.
Bueno, es hora de escribir el código que procesará los datos llegados de la página Web, pero para esto primero hay que crear dos entradas en el "Mapa de comandos", algo parecido al "Mapa de mensajes" que crea el Class Wizard cada vez que agregamos un mensaje de Windows a nuestra aplicaciones. El Class Wizard agrega una línea en el mapa de mensajes para asociar un método con ese mensaje en particular; pero para esta dll ISAPI el proceso no es automático, sino que deberemos agregarlo a mano por cada función que contenga la clase.
En el archivo de implementación de la clase CDcpejemploExtension, justo antes del constructor, encontrará un bloque de código encerrado entre las macros BEGIN_PARSE_MAP(...) y END_PARSE_MAP(...). Puntualmente verá lineas como:
ON_PARSE_COMMAND(Default, CDcpejemploExtension, ITS_EMPTY)
DEFAULT_PARSE_COMMAND(Default, CDcpejemploExtension)
Estas mácros permiten especificar una función que manipulará datos llegados del cliente, indicando que tipo de parámetros son y cuales.
Por defecto ya existe una función llamada "Default", que no recibe parámetros y que no sirve para mucho. Tenemos que agregar dos nueva líneas, como se muestra a continuación, (marcadas en amarillo):
// command-parsing map BEGIN_PARSE_MAP(CDcpejemploExtension, CHttpServer) // TODO: insert your ON_PARSE_COMMAND() and // ON_PARSE_COMMAND_PARAMS() here to hook up your commands. // For example: ON_PARSE_COMMAND(Default, CDcpejemploExtension, ITS_EMPTY) DEFAULT_PARSE_COMMAND(Default, CDcpejemploExtension) //Mapa de comandos de la función, (la única que hay en esta extensión). ON_PARSE_COMMAND(Login, CDcpejemploExtension, ITS_PSTR ITS_PSTR) ON_PARSE_COMMAND_PARAMS("user_id user_pass") //Los parámetros que recibe. END_PARSE_MAP(CDcpejemploExtension) |
Recuerde: por cada función que tenga esta extensión deberá escribir dos líneas como estas además de la implementación, (que por cierto aún no lo hemos hecho, ya viene..).
Con ON_PARSE_COMMAND se especifica la función, Login, luego a que clase pertenece, (CDcpejemploExtension y los tipo de parámetros ITS_PSTR ITS_PSTR, (no se separan con comas, simplemente deje un espacio por cada tipo).
Los tipos de parámetros posibles son:
ITS_EMPTY: En caso de no haber argumentos.
ITS_PSTR: Puntero a cadena.
ITS_I2: Entero corto.
ITS_I4: Entero largo.
ITS_R4: Flotante.
ITS_R8: Doble.
ITS_RAW: Otro tipo de dato enviado al servidor, por ejemplo puntero a void.
Y en ON_PARSE_COMMAND_PARAMS se indican cada uno de los nombres de los parámetros, (los nombres que tienen en el formulario de la página HTML), separados por espacios y todo entre comillas.
Una vez escrito ésto en el mapa de comandos, ahora agregue la función Login, que retorna void y recibe los parámetros: CHttpServerContext *pCtxt, LPCTSTR pstrId, LPCTSTR pstrPass.
Escriba:
void CDcpejemploExtension::Login(CHttpServerContext *pCtxt, LPCTSTR pstrId, LPCTSTR pstrPass) { int i=0; short int edad=0; BOOL encontrado=FALSE; //Escribe los tags <html> y <body> en la futura página de respuesta. StartContent(pCtxt); //Coloca el título al navegador, para esto hay que sobreescribir la función virtual GetTitle() WriteTitle(pCtxt); //Escribo un texto como título, (de la página no del navegador, no es el tag <TITLE> *pCtxt<<"<font face=\"Tahoma\" size = 4 color=#0000FF>"; *pCtxt<<"Resultado de la busqueda</font>"; //Si ingreso algo en ambos cuadros de texto if ((strlen(pstrId) > 0) && (strlen(pstrPass)>0)) { //Itero hasta encontrar el usuario o llegar al final del arreglo de CUsuarios while ((i<5) && (!encontrado)) { //Si el usuario ingresado coincide con el de la lista if (strcmp(m_ListaUsuarios[i]->m_strID, pstrId)==0) //Me fijo si además coincide la clave. if (strcmp(m_ListaUsuarios[i]->m_strPass, pstrPass)==0) { //Si coincide todo, pongo la variable bandera en TRUE encontrado=TRUE; //y tomo la edad para mostrarla. edad=m_ListaUsuarios[i]->m_iEdad; } i++; } //fin del while if (encontrado) //Luego de salir del while me fijo si salió por encontrado=TRUE { //Si, entonces muestro la info. *pCtxt <<"<p>Encontrado</p>"; *pCtxt <<"<p><b>Usuario: </b><i>"<< pstrId<<" </i> usted tiene "; *pCtxt <<"<i>"<< edad << "</i> años</p>"; } else //No, entonces informo. *pCtxt<<"<p>Usuario no encontrado</p>"; } //fin del 1º if else //¿Dejo algo en blanco? { *pCtxt << "<p><font face=\"Tahoma\" size = 5 color = #FF0000> Faltan datos"; *pCtxt << "</font></p>"; } EndContent(pCtxt); //Los tags </body> y </html> } |
A la función llegan: un puntero a CHttpContext y dos punteros a cadenas que son los datos enviados desde el formulario.
Con el puntero a CHttpContext encaminamos la salida como si fuera el objeto cout, con los operadores de insercción <<, dando ésto origen a una nueva página HTML.
La función WriteTitle(pCtxt) le coloca el título a la respuesta, o sea, pone el texto en la barra de la ventana del navegador que mostrará la respuesta HTML. Para poder usarla hay que sobreescribir la función virtual GetTitle(), entonces pulse con el derecho en el Class View sobre nuestra clase derivada CHttpServer y seleccione Add Virtual Function. Agregue la función GetTitle() y escriba:
//Esta función virtual la agregué para poder colocar un título, (sería el tag <TITLE>). LPCTSTR CDcpejemploExtension::GetTitle() const { return "Respuesta"; } |
Listo, ya puede construir el proyecto, lo que dará como resultado un archivo dcpejemplo.dll que deberá copiar a la carpeta "scripts" donde tiene configurado los directorios virtuales, (posiblemente: c:\Inetpub).
Probando la extensión.
Verifique con el IIS, ("Administrador de servicios de internet") que el servidor de HTTP está en ejecución, es probable que, si modificó el proyecto y construyó una nueva versión de la dll, haya tenido que "detener" el servicio de HTTP para poder copiar la dll nueva en el directorio scripts. Así que si verificó eso, ejecute el navegador de internet y compruebe que en "Archivo -> Trabajar sin conexión" la opción NO ESTA MARCADA, sino no funcionará.
Ingrese la siguiente dirección: http://localhost/login.htm y aparecerá la página que creamos ingrese valores que sepa que existen en el arreglo de usuarios en la dll, y pulse el botón:
Si todo funciona bien debería obtener un resultado similar a este:
Descargar fuente del ejemplo: dcpejemplo.zip (26 Kb).