21 noviembre 2008

El componente ZipMaster

Si en el anterior artículo vimos como comprimir y descomprimir archivos con el componente ZipForge cuyo único inconveniente es que es comercial, hoy vamos a ver como hacer lo mismo con el componente ZipMaster, el cual es gratuito (con licencia LGPL).

El único inconveniente que tiene este componente es que necesita tener una librería dinámica al lado de nuestro ejecutable: DelZip179.dll. Según sus autores, podemos definir una directiva para añadir la librería de manera estática, aunque yo no he podido conseguirlo como comentaré más adelante.

DESCARGANDO EL COMPONENTE DE SU PAGINA WEB

La página oficial del componente ZipMaster es esta:

http://www.delphizip.org/


La última versión a fecha de este artículo es la 1.79. Soporta desde Delphi 5 hasta Delphi 2007.

Nos bajamos el archivo zm179setup1004.exe que tiene un tamaño de 3,07 MB y lo instalamos:


Después de instalarlo debemos instalar el paquete DPK en nuestra versión de Delphi habitual con los siguientes pasos:

1. Seleccionamos File -> Open y en el cuadro de búsqueda que aparece seleccionamos en el campo Tipo los archivos de tipo DPK:


2. En mi caso voy a instalar la versión ZipMaster11.dpk que corresponde a la versión 2007 de Delphi.

3. Seleccionamos el paquete abierto con el botón derecho del ratón y seleccionamos Install:


4. Si todo ha ido bien aparecerá este mensaje:


Una vez instalado debe aparecer en la paleta de componentes:


AÑADIENDO EL COMPONENTE A NUESTRO PROYECTO

Insertamos el componente ZipMaster en un formulario y lo llamamos Zip para abreviar:


También debemos vincular el directorio de búsqueda del proyecto al directorio donde se encuentra la unidad ZipMaster. Esto se hace seleccionando Proyect -> Options y en la sección Directories/Conditionals pulsamos el botón […] a la derecha del campo Search Path y añadimos:

D:\CodeGear\RAD Studio\5.0\Componentes\ZipMaster\

Suponiendo que sea ese el directorio donde he instalado el componente ZipMaster. También sería una buena costumbre copiar la librería DelZip179.dll del directorio:

D:\CodeGear\RAD Studio\5.0\Componentes\ZipMaster\DLL\

a donde tengamos nuestro proyecto, aunque no es necesario ya que se encuentra en C:\Windows\System32\. Es bueno llevarlo al lado del ejecutable cuando tenemos que instalar el programa en otros ordenadores.

COMO COMPRIMIR ARCHIVOS CON ZIPMASTER

Ahora supongamos que quiero comprimir un par de hojas de cálculo que se encuentran en la carpeta D:\prueba\:


Este sería el proceso para comprimir todos los archivos de esa carpeta:

Zip.DLLDirectory := ExtractFilePath( Application.ExeName );
Zip.ZipFileName := 'D:\prueba\calculos.zip';
Zip.TempDir := 'D:\prueba\';
Zip.FSpecArgs.Clear;
Zip.FSpecArgs.Add( 'D:\prueba\*.*' );
try
Zip.Add;
except
raise exception.Create( 'Error al comprimir los archivos.' );
end;

He realizado los siguientes pasos:

1º Le he especificado el directorio donde se encuentra la librería dinámica DelZip129.dll con la propiedad DLLDirectory.

2º Con la propiedad ZipFileName le indico como se va a llamar el archivo zip que voy a crear.

3º Aunque no es obligatorio, si es recomendable decirle el directorio temporal donde procesar los archivos temporales (TempDir). Lo normal es que sea el mismo directorio donde se están comprimiendo los archivos, por si se detiene el proceso y deja archivos temporales.

4º Por último vamos añadiendo los archivos que queremos comprimir:

Zip.FSpecArgs.Clear;
Zip.FSpecArgs.Add( 'D:\prueba\*.*' );

También podemos añadir una contraseña al archivo comprimido de este modo:

Zip.Password := '1234';

CREANDO ARCHIVOS AUTOEXTRAIBLES

Para crear un archivo comprimido y autoextraible hay que introducir en nuestro formulario el componente ZipSFX:


Suponiendo que el componente se llame ZipSFX escribimos el siguiente código que convierte un archivo zip ya comprimido en uno ejecutable:

ZipSFX.SFXPath := 'D:\CodeGear\RAD Studio\5.0\Componentes\ZipMaster\Res\';
ZipSFX.SourceFile := 'D:\prueba\calculos.zip';
ZipSFX.TargetFile := 'D:\prueba\calculos.exe';
ZipSFX.Convert;

El archivo que hace de autoextraible procede del directorio:

..\ZipMaster\Res\

El cual contiene dentro el archivo sfx_std.zip que hay que descomprimir en el mismo directorio.

Al ejecutar el archivo autoextraible aparece esta ventana:


Supuestamente también nos permite crear el archivo autoextraible en varios idiomas, incluido el español. Esto es lo que habría que añadir:

ZipSFX.SFXLanguage := ‘es’;

Lo que ocurre es que al ejecutarlo da este error:


Y ya he comprobado de que se encuentran estos archivos en ese directorio:

Dzsfxes.res
Dzsfxes.bin

Pero no hay manera de que me haga caso.

COMO DESCOMPRIMIR ARCHIVOS CON ZIPMASTER

Para descomprimir uno o más archivos con ZipMaster hacemos esto:

Zip.DLLDirectory := ExtractFilePath( Application.ExeName );
Zip.ZipFileName := 'D:\prueba\calculos.zip';
Zip.TempDir := 'D:\prueba\';
Zip.ExtrBaseDir := 'D:\prueba\';
Zip.FSpecArgs.Clear;
Zip.FSpecArgs.Add( '*.*' );
try
Zip.Extract;
except
raise exception.Create( 'Error al descomprimir los archivos.' );
end;

INTENTANDO AÑADIR LA LIBRERÍA DELZIP179.DLL

En la documentación de este componente nos indica que si añadimos la directiva:

{$DEFINE STATIC_LOAD_DELZIP_DLL}

en nuestro proyecto ya no necesitamos la librería DelZip179.dll al lado de nuestro ejecutable. Aunque he probado a quitar esta DLL y al ejecutarlo me da el error:


Que me lo expliquen.

CONCLUSIONES

Pese a las dos cosas que he intentado hacer y que no me han funcionado he de reconocer que el rendimiento de este componente es excelente y tiene una cantidad de opciones impresionante.

Pruebas realizadas en RAD Studio 2007.

14 noviembre 2008

Comprimir y descomprimir archivos con ZipForge

Aunque hay muchos componentes de terceros para Delphi que comprimen y descomprimen archivos zip con total naturalidad hay que reconocer que muchos de ellos necesitan librerías DLL externas. Sin embargo el componente ZipForge permite manipular archivos zip sin ninguna librería adicional. Todo va dentro de nuestro ejecutable.

Este componente es comercial y se vende al precio de 49 € (sin el código fuente). Para probarlo podemos bajar una versión personal para uso no comercial desde su página web:

http://componentace.com/zip_component_zip_delphi_zipforge.htm


Lo bueno de este componente es que está disponible para todas las versiones de Delphi:

En mi caso me he bajado la versión para RAD Studio 2007, cuya instalación va comprimida en un archivo zip de 2,81 MB.

INSTALANDO EL COMPONENTE

Para instalarlo sólo hay que descomprimir el archivo y ejecutar Install.exe, para luego ir pulsando el botón Next hasta que finalice la instalación:


Lo bueno de esta instalación es que no hay que instalar paquetes DPK ya que lo hace automáticamente. Sólo hay que cerrar Delphi, volver a abrirlo y veremos este mensaje al arrancar:


Cuando compremos el producto y lo registremos desaparecerá este mensaje. Al crear o abrir un nuevo proyecto ya tenemos visible el componente ZipForge en la paleta de componentes:


COMPRIMIR ARCHIVOS CON ZIPFORGE

Insertamos el componente en el formulario y le ponemos en su propiedad name el nombre Zip para simplificar:


Supongamos que tengo en la carpeta D:\prueba estos tres archivos:


Para comprimir estos archivos en un solo archivo zip hacemos lo siguiente:

With Zip do
begin
FileName := 'd:\prueba\documentos.zip';
OpenArchive( fmCreate );
BaseDir := 'd:\prueba\';
TempDir := 'd:\prueba\';
AddFiles( '*.pdf' );
CloseArchive;
end;

Con FileName le hemos dicho como va a llamarse el archivo zip que vamos a crear. Después creamos el archivo con OpenArchive y le decimos con la propiedad BaseDir desde donde vamos a comprimir los archivos.

No es necesario decirle también la propiedad TempDir (directorio temporal de compresión/decompresión) aunque recomiendo utilizar el mismo directorio para que no deje archivos temporales en Windows en caso de que se interrumpa el proceso de compresión/descompresión.

Por último sólo hay que decirle mediante AddFiles los archivos que vamos a comprimir (admitiendo incluso *.*) y cerramos el archivo creado.

Esta sería una compresión de archivos normal pero tenemos muchas más opciones. Por ejemplo, le podemos poner una contraseña al archivo zip escribiendo esto antes del comando AddFiles:

Password := '1234';

Si queremos asegurarnos de que los archivos se han comprimido correctamente podemos ejecutar esto:

AddFiles( '*.*' );

try
TestFiles('*.*');
except
Application.MessageBox( 'Error al comprimir los archivos.',
'Atención', MB_ICONSTOP );
end;

CloseArchive;

Otra opción que podemos elegir es el nivel de compresión:

CompresionLevel := clMax;

Permite todos estos valores:

clMax -> es la que más comprime pero la más lenta
clNormal -> compresión normal
clFastest -> es la compresión más rápida pero la que menos comprime
clNone -> no comprime nada, sólo empaqueta los archivos.

Si queremos controlar el proceso de compresión con una barra de progreso tenemos que añadir este código en el evento OnFileProgress:

procedure TForm1.ZipFileProgress(Sender: TObject; FileName: WideString;
Progress: Double; Operation: TZFProcessOperation;
ProgressPhase: TZFProgressPhase; var Cancel: Boolean);
begin
if ProgressPhase = ppProcess then
ProgressBar1.Position := Round(Progress);
end;

Lo que contiene el valor Progress no es el incremento total de la compresión sino el porcentaje de cada archivo que está comprimiendo. Por lo tanto sería conveniente hacer esto antes de empezar:

ProgressBar1.Max := 100;

Por último y no menos importante, también nos permite comprimir archivos desde un stream, desde un buffer de memoria o una cadena de texto:

procedure AddFromStream(FileName: WideString; Stream: TStream;
CopyToBuffer: Boolean = True; Position: Integer = 0;
Count: Integer = 0; Attr: Integer = faArchive; DateTime: TDateTime = 0);

procedure AddFromBuffer(FileName: WideString; const Buffer; Count: Integer;
Attr: Integer = faArchive; DateTime: TDateTime = 0);

procedure AddFromString(FileName: WideString; Text: String;
Attr: Integer = faArchive; DateTime: TDateTime = 0);

Algo sin duda muy interesante para no tener que crear archivos intermedios.

CREANDO ARCHIVOS AUTOEJECUTABLES

Para comprimir un archivo y hacerlo autoejecutable debemos utilizar un ejecutable que trae predeterminado el componente ZipForge y que viene en el directorio:

\ComponentAce\ZipForge\SFXStub\SFXStub.exe

Esa ruta hay que indicársela antes de crearlo:

With Zip do
begin
FileName := 'd:\prueba\documentos.zip';
OpenArchive( fmCreate );
BaseDir := 'd:\prueba\';
TempDir := 'd:\prueba\';
AddFiles( '*.*' );
CloseArchive;
SFXStub := 'D:\CodeGear\RAD Studio\5.0\Componentes\ZipForge\SFXStub\SFXStub.exe';
MakeSFX( 'd:\prueba\documentos.exe' );
end;

Al hacer doble clic sobre el ejecutable nos pedirá la ruta de descompresión:


DESCOMPRIMIR ARCHIVOS CON ZIPFORGE

Descomprimir archivos es todavía más fácil que comprimirlos:

With Zip do
begin
FileName := 'd:\prueba\documentos.zip';
OpenArchive( fmOpenRead );
BaseDir := 'd:\prueba\';
TempDir := 'd:\prueba\';
ExtractFiles( '*.*' );
CloseArchive;
end;

Si nos fijamos en las propiedades que tiene el componente ZipForge en el inspector de objetos podemos configurar si queremos que cree carpetas al descomprimir (si las hay) o elegir por ejemplo si queremos sobrescribir archivos:


Un componente como este se hace casi imprescindible para dar a nuestros programas funcionalidades tales como crear copias de seguridad de bases de datos, descomprimir archivos de configuración en directorios remotos o incluso proteger nuestros gráficos y recursos dentro de un zip con contraseña para que no puedan ser utilizados por otro programador.

Pruebas realizadas en RAD Studio 2007.

07 noviembre 2008

Crea tu propio servidor HTTP (y 4)

Vamos a terminar de ver las características principales del componente TIdHTTPServer viendo como validar a los usuarios del mismo utilizando las cookies del navegador.

Para el que no las conozca, las cookies son archivos temporales que una aplicación web puede crear en el navegador del cliente para guardar estados de sesión. En nuestro caso vamos a guardar el usuario y la contraseña del usuario que ha entrado.

REPROGRAMANDO EL EVENTO ONCOMMANDGET

En el evento OnCommandGet vamos a mostrar en la ventana del servidor las cookies que devuelve el navegador del cliente y también vamos a dar la posibilidad de entrar a la página principal del foro (foro.html) cuando los usuarios sean validados:

procedure TFServidorHTTP.ServidorCommandGet(AContext: TIdContext;
ARequestInfo: TIdHTTPRequestInfo; AResponseInfo: TIdHTTPResponseInfo);
var
sDocumento: String;
i: Integer;
begin
if ARequestInfo.Cookies.Count > 0 then
begin
for i := 0 to ARequestInfo.Cookies.Count-1 do
Log.Lines.Add( 'cookie: ' + ARequestInfo.Cookies[i].CookieName + ': ' +
ARequestInfo.Cookies[i].Value );
end;

if ARequestInfo.Document = '/registrar' then
RegistrarUsuario( ARequestInfo, AResponseInfo );

if ARequestInfo.Document = '/entrar' then
Entrar( ARequestInfo, AResponseInfo );

if ARequestInfo.Document = '/enviarmensaje' then
EnviarMensaje( ARequestInfo, AResponseInfo );

if ARequestInfo.Document = '/foro.html' then
begin
Foro( ARequestInfo, AResponseInfo );
Exit;
end;

// ¿Va a entrar a la página principal?
if ARequestInfo.Document = '/' then
AResponseInfo.ServeFile( AContext, ExtractFilePath( Application.ExeName ) + 'index.html' )
else
begin
// Cargamos la página web que vamos a enviar
sDocumento := ExtractFilePath( Application.ExeName ) +
Copy( ARequestInfo.Document, 2, Length( ARequestInfo.Document ) );

// ¿Existe la página que ha solicitado?
if FileExists( sDocumento ) then
AResponseInfo.ServeFile( AContext, sDocumento )
else
// No hemos encontrado la página
AResponseInfo.ResponseNo := 404;
end;

AResponseInfo.CloseConnection := True;
end;

He añadido como novedades la posibilidad de ver las cookies que tiene el cliente en el navegador:

if ARequestInfo.Cookies.Count > 0 then
begin
for i := 0 to ARequestInfo.Cookies.Count-1 do
Log.Lines.Add( 'cookie: ' + ARequestInfo.Cookies[i].CookieName + ': ' +
ARequestInfo.Cookies[i].Value );
end;

Y suministrar la página del foro donde se van a escribir los mensajes:

if ARequestInfo.Document = '/foro.html' then
begin
Foro( ARequestInfo, AResponseInfo );
Exit;
end;

Ahora debemos modificar la página de entrada para crear las cookies.

CREANDO LAS COOKIES EN EL NAVEGADOR DEL USUARIO

Vamos a modificar nuestro procedimiento de entrar para que cuando el usuario sea validado cree en el navegador del cliente dos cookies para almacenar el usuario y su contraseña:

procedure TFServidorHTTP.Entrar( ARequestInfo: TIdHTTPRequestInfo;
AResponseInfo: TIdHTTPResponseInfo );
var
sNombre, sPassword, sError, sUsuarios: String;
Usuarios: TStringList;
begin
sNombre := ARequestInfo.Params.Values['nombre'];
sPassword := ARequestInfo.Params.Values['password'];

// Abrimos la lista de usuarios
sUsuarios := ExtractFilePath( Application.ExeName ) + 'usuarios.txt';
Usuarios := TStringList.Create;

if FileExists( sUsuarios ) then
Usuarios.LoadFromFile( sUsuarios );

// Comprobamos si el usuario ya ha sido dado de alta en la lista
if Usuarios.Values[sNombre] = '' then
sError := '<h3>El usuario no existe.</h3>'
else
if Usuarios.Values[sNombre] <> sPassword then
sError := '<h3>La contraseña es incorrecta.</h3>';

if sError <> '' then
begin
AResponseInfo.ContentText := sError;
AResponseInfo.WriteContent;
end
else
begin
with AResponseInfo.Cookies.Add do
begin
CookieName := 'usuario';
Value := sNombre;
end;

with AResponseInfo.Cookies.Add do
begin
CookieName := 'password';
Value := sPassword;
end;

AResponseInfo.Redirect( 'foro.html' );
AResponseInfo.WriteContent;
end;

Usuarios.Free;
end;

Cuando el usuario ha sido validado le creamos las dos cookies y redireccionamos al cliente a la página del foro.

CREANDO LA PÁGINA WEB DEL FORO

La página web del foro va a ser la siguiente:


La página principal del foro contiene en su parte superior el nombre el usuario que ha entrado y la lista de mensajes que han enviado los usuarios. En la parte inferior muestro también el número de mensajes que hay en el foro y un pequeño formulario para escribir un nuevo mensaje.

Todo esto lo gestiono con un nuevo procedimiento llamado Foro que genera toda la página web:

procedure TFServidorHTTP.Foro( ARequestInfo: TIdHTTPRequestInfo;
AResponseInfo: TIdHTTPResponseInfo );
var
i: Integer;
Mensajes, Usuarios: TStringList;
s, sUsuario, sPassword, sMensajes, sUsuarios: String;
begin
// Antes de entrar en el foro comprobamos si el usuario está registrado.
// Para ello leemos las cookies del navegador del cliente
i := ARequestInfo.Cookies.GetCookieIndex(0,'usuario');
if i > -1 then
sUsuario := ARequestInfo.Cookies[i].Value;

i := ARequestInfo.Cookies.GetCookieIndex(0,'password');
if i > -1 then
sPassword := ARequestInfo.Cookies[i].Value;

// Cargamos la lista de usuarios y verificamos su acceso
sUsuarios := ExtractFilePath( Application.ExeName ) + 'usuarios.txt';
Usuarios := TStringList.Create;
if FileExists( sUsuarios ) then
Usuarios.LoadFromFile( sUsuarios );

if Usuarios.IndexOf( sUsuario + '=' + sPassword ) = -1 then
begin
AResponseInfo.Redirect( 'index.html' );
AResponseInfo.WriteContent;
Usuarios.Free;
end
else
Usuarios.Free;

sMensajes := ExtractFilePath( Application.ExeName ) + 'mensajes.txt';
Mensajes := TStringList.Create;

if FileExists( sMensajes ) then
Mensajes.LoadFromFile( sMensajes );

s := '<h2>Listado de mensajes</h2><p>';

// Mostramos el usuario logeado

i := ARequestInfo.Cookies.GetCookieIndex(0,'usuario');
if i > -1 then
s := s + '<h2>Usuario: ' + ARequestInfo.Cookies[i].Value + '</h2>';

for i := 0 to Mensajes.Count - 1 do
s := s + Mensajes[i] + '<p>';

s := s + '<h4>Hay un total de ' + IntToStr( Mensajes.Count ) +
' mensajes</h4>';

s := s + '<p><h4>Escribir un nuevo mensaje</h4>';
s := s + '<form name="mensaje" action="enviarmensaje" method="post">';
s := s + '<label for="nombre">Título: </label><br>';
s := s + '<input name="titulo" size="40" type="text"><br>';
s := s + '<label for="nombre">Mensaje: </label><br>';
s := s + '<textarea name="mensaje" rows="3" cols="40"></textarea>
<p><br>';
s := s + '<input value="Enviar" type="submit"></form>';

Mensajes.Free;
AResponseInfo.ContentText := s;
end;

Antes de poder listar los mensajes del foro tenemos que validar al usuario leyendo las cookies y comprobando si esta en la lista de usuarios. Después muestro los mensajes que ha enviado cada usuario.

Aunque con una validación como esta es suficiente para controlar la entrada de usuarios, para crear un foro en condiciones habría que crear más cookies para identificadores de sesión, control del tiempo que lleva el usuario así como un poco de encriptación en las cookies para evitar el robo de las claves mediante sniffers.

También habría que guardar la información del foro en una base de datos fiable como Internase, Firebird, etc. Esto unido a unas buenas hojas de estilo CSS nos permitirá crear unas aplicaciones web pequeñas y potentes.

Y este sería el procedimiento EnviarMensaje:

procedure TFServidorHTTP.EnviarMensaje( ARequestInfo: TIdHTTPRequestInfo;
AResponseInfo: TIdHTTPResponseInfo );
var
i: Integer;
sTitulo, sMensaje, sError, sMensajes, sUsuario: String;
Mensajes: TStringList;
begin
sTitulo := ARequestInfo.Params.Values['titulo'];
sMensaje := ARequestInfo.Params.Values['mensaje'];

i := ARequestInfo.Cookies.GetCookieIndex(0,'usuario');
if i > -1 then
sUsuario := ' ' + ARequestInfo.Cookies[i].Value;

if sTitulo = '' then
sError := '<h3>Debe introducir el título del mensaje</h3>';

if sMensaje = '' then
sError := '<h3>Debe introducir el mensaje</h3>';

// Abrimos la lista de mensajes
sMensajes := ExtractFilePath( Application.ExeName ) + 'mensajes.txt';
Mensajes := TStringList.Create;

if FileExists( sMensajes ) then
Mensajes.LoadFromFile( sMensajes );

if sError <> '' then
AResponseInfo.ContentText := sError
else
begin
Mensajes.Add( '<h4>' + DateTimeToStr( Now ) + sUsuario + '</h4><p>' +
UpperCase( sTitulo ) + ': ' + sMensaje );
Mensajes.SaveToFile( sMensajes );
AResponseInfo.Redirect( 'foro.html' );
end;

AResponseInfo.WriteContent;
Mensajes.Free;
end;

Este procedimiento es el utilizado por los usuarios para enviar un mensaje al foro.

Con esto termino estos artículos relacionados con el componente TIdHTTPServer aunque no descarto escribir más sobre el mismo si descubro más utilidades interesantes como puede ser por ejemplo la creación de páginas web seguras con SSL.

Pruebas realizadas en RAD Studio 2007.

31 octubre 2008

Crea tu propio servidor HTTP (3)

Si bien hemos aprendido a que el navegador pida nombre de usuario y clave para entrar en nuestras webs privadas, la forma más natural de entrar a una página web suele ser dándose de alta en un formulario HTML para luego entrar con sus datos.

En esta ocasión no vamos a utilizar por ahora las propiedades de autenticación que vimos en artículos anteriores con AResponseInfo.AuthRealm ni vamos controlar la sesión con la clase TIdHTTPSession.

Vamos a comenzar a ver un ejemplo de cómo crear nuestro propio foro. En este artículo vamos a controlar el registro y la entrada de usuarios.

CREANDO LAS PÁGINAS WEB DEL FORO

Para crear un foro necesitamos una página web de entrada que permita hacer login al usuario así como darse de alta en nuestro foro. Esta va a ser nuestra página web de entrada:

El código fuente de la página se puede hacer con el bloc de notas de Windows (clic para ampliar):


La página contiene un formulario que recoge el usuario y su contraseña para luego pulsar el botón Enviar. Esta página hay que guardarla con el nombre index.html.

Cuando el usuario pulse el enlace Registrar entonces saltará a esta otra página:


Cuyo código fuente es el siguiente:


Ahora es cuando tenemos que entrar en faena y programar nuestro servidor HTTP.

CREANDO LA APLICACIÓN SERVIDOR

La ventana del servidor es prácticamente la misma que vimos en los artículos anteriores:


Lo que vamos a cambiar va a ser el evento OnCommandGet del componente TIdHTTPServer:

procedure TFServidorHTTP.ServidorCommandGet(AContext: TIdContext;
ARequestInfo: TIdHTTPRequestInfo; AResponseInfo: TIdHTTPResponseInfo);
var
sDocumento: String;
begin
Log.Lines.Add( ARequestInfo.RemoteIP + ': ' +
ARequestInfo.Command + ARequestInfo.Document );

Log.Lines.Add( 'Parámetros: ' + ARequestInfo.Params.Text );

if ARequestInfo.Document = '/registrar' then
RegistrarUsuario( ARequestInfo, AResponseInfo );

if ARequestInfo.Document = '/entrar' then
Entrar( ARequestInfo, AResponseInfo );

// ¿Va a entrar a la página principal?
if ARequestInfo.Document = '/' then
AResponseInfo.ServeFile( AContext, ExtractFilePath( Application.ExeName ) + 'index.html' )
else
begin
// Cargamos la página web que vamos a enviar
sDocumento := ExtractFilePath( Application.ExeName ) +
Copy( ARequestInfo.Document, 2, Length( ARequestInfo.Document ) );

// ¿Existe la página que ha solicitado?
if FileExists( sDocumento ) then
AResponseInfo.ServeFile( AContext, sDocumento )
else
// No hemos encontrado la página
AResponseInfo.ResponseNo := 404;
end;

AResponseInfo.CloseConnection := True;
end;

Este evento se compone de las siguientes partes:

1º Mostramos en la ventana del servidor que página web nos solicita el usuario y los parámetros de la misma:

Log.Lines.Add( ARequestInfo.RemoteIP + ': ' +
ARequestInfo.Command + ARequestInfo.Document );

Log.Lines.Add( 'Parámetros: ' + ARequestInfo.Params.Text );

Los parámetros serán los datos que el usuario ha rellenado en el formulario web antes de pulsar el botón Enviar.

2º Si el usuario pulsa los botones de registrarse en la página o hacer login entonces envío a cada uno a su procedimiento correspondiente para dar más claridad al código:

if ARequestInfo.Document = '/registrar' then
RegistrarUsuario( ARequestInfo, AResponseInfo );

if ARequestInfo.Document = '/entrar' then
Entrar( ARequestInfo, AResponseInfo );

3º En caso de que el usuario haya solicitado otra página al servidor se la mandamos normalmente:

// ¿Va a entrar a la página principal?
if ARequestInfo.Document = '/' then
AResponseInfo.ServeFile( AContext, ExtractFilePath( Application.ExeName ) + 'index.html' )
else
begin
// Cargamos la página web que vamos a enviar
sDocumento := ExtractFilePath( Application.ExeName ) +
Copy( ARequestInfo.Document, 2, Length( ARequestInfo.Document ) );

// ¿Existe la página que ha solicitado?
if FileExists( sDocumento ) then
AResponseInfo.ServeFile( AContext, sDocumento )
else
// No hemos encontrado la página
AResponseInfo.ResponseNo := 404;
end;

AResponseInfo.CloseConnection := True;
end;

CONTROLANDO EL REGISTRO DE USUARIOS

El procedimiento encargado de dar de alta los usuarios tiene que comprobar si el usuario ya rellenado correctamente su nombre, la contraseña y si las contraseñas coinciden:

procedure TFServidorHTTP.RegistrarUsuario( ARequestInfo: TIdHTTPRequestInfo;
AResponseInfo: TIdHTTPResponseInfo );
var
sNombre, sPassword, sPassword2, sError, sUsuarios: String;
Usuarios: TStringList;
begin
sNombre := ARequestInfo.Params.Values['nombre'];
sPassword := ARequestInfo.Params.Values['password'];
sPassword2 := ARequestInfo.Params.Values['password2'];

if sPassword <> sPassword2 then
sError := '<h3>Las contraseñas no coinciden.</h3>';

if sPassword2 = '' then
sError := '<h3>Debe repetir la contraseña.</h3>';

if sPassword = '' then
sError := '<h3>No ha introducido la contraseña.</h3>';

if sNombre = '' then
sError := '<h3>No ha introducido el nombre del usuario.</h3>';

// Abrimos la lista de usuarios
sUsuarios := ExtractFilePath( Application.ExeName ) + 'usuarios.txt';
Usuarios := TStringList.Create;

if FileExists( sUsuarios ) then
Usuarios.LoadFromFile( sUsuarios );

// Comprobamos si el usuario ya ha sido dado de alta en la lista
if sError = '' then
begin
if Usuarios.Values[sNombre] <> '' then
sError := '<h3>El usuario ya existe. Elija otro nombre.</h3>';
end;

if sError <> '' then
AResponseInfo.ContentText := sError
else
begin
Usuarios.Add( sNombre + '=' + sPassword );

AResponseInfo.ContentText := '<h3>Usuario registrado correctamente:<p>' +
'Nombre: ' + sNombre + '<p>' + 'Contraseña: ' + sPassword + '<p>' +
'<a href="http://www.blogger.com/index.html">Entrar al foro.</a></p></h3>';

Usuarios.SaveToFile( sUsuarios );
end;

AResponseInfo.WriteContent;
Usuarios.Free;
end;

En caso de error mostraría su mensaje correspondiente:


En el caso de que se hayan escrito bien todos los datos lo que hago es guardar la lista de usuarios y sus contraseñas en un archivo de texto que se llamará usuarios.txt. También compruebo si el usuario ya ha sido dado de alta con anterioridad.

Una vez que ha sido dado de alta lo muestro en pantalla:


A su vez vemos como la ventana del servidor va controlando lo que manda el navegador:


Al pulsar el enlace Entrar al foro volverá a la pantalla principal para hacer login.

CONTROLANDO LA ENTRADA DE USUARIOS

El procedimiento encargado de entrar en nuestro foro es parecido y más sencillo que el de registro:

procedure TFServidorHTTP.Entrar( ARequestInfo: TIdHTTPRequestInfo;
AResponseInfo: TIdHTTPResponseInfo );
var
sNombre, sPassword, sError, sUsuarios: String;
Usuarios: TStringList;
begin
sNombre := ARequestInfo.Params.Values['nombre'];
sPassword := ARequestInfo.Params.Values['password'];

// Abrimos la lista de usuarios
sUsuarios := ExtractFilePath( Application.ExeName ) + 'usuarios.txt';
Usuarios := TStringList.Create;

if FileExists( sUsuarios ) then
Usuarios.LoadFromFile( sUsuarios );

// Comprobamos si el usuario ya ha sido dado de alta en la lista
if Usuarios.Values[sNombre] = '' then
sError := '<h3>El usuario no existe.</h3>'
else
if Usuarios.Values[sNombre] <> sPassword then
sError := '<h3>La contraseña es incorrecta.</h3>';

if sError <> '' then
AResponseInfo.ContentText := sError
else
AResponseInfo.ContentText := '<h3>Bienvenido al foro ' + sNombre + '<p>' +
'<a href="http://www.blogger.com/index.html">Salir</a></h3>';

AResponseInfo.WriteContent;
Usuarios.Free;
end;

Sólo hay que asegurarse de que el usuario esté dado de alta en nuestra lista y luego lo dejamos pasar:


Todos los usuarios datos de alta quedan almacenados en el archivo usuarios.txt:


En el próximo artículo vamos a implementar el sistema visualización y envío de mensajes al foro por parte de cada usuario.

No he incluido las diferencias respecto a Delphi 7 porque los cambios son los mismos respecto a los anteriores artículos (ServeFile).

Pruebas realizadas en RAD Studio 2007.

24 octubre 2008

Crea tu propio servidor HTTP (2)

Si en el anterior artículo vimos como validar la entrada de usuarios utilizando el usuario y la contraseña que pide el navegador del cliente, ahora vamos a ver como mantener el estado del usuario permanentemente en el servidor.

El estado en el servidor suele utilizarse por ejemplo para comprobar el tiempo que lleva conectado, el número de peticiones que ha realizado o para guardar las últimas consultas que ha realizado en el servidor. Esto es muy utilizado en los juegos RPG online donde suele guardarse la puntuación, energía, objetos, etc.

CREANDO UNA SESION

Los componentes Indy tienen un clase asociada el protocolo HTTP llamada TIdHTTPSession. Para utilizar este objeto hay que crearlo dentro de la lista de sesiones que tiene la clase TIdHTTPServer.

En el ejemplo que he realizado para el evento OnCommandGet voy a guardar en la sesión la fecha y la hora de cuando comenzó la sesión ese usuario:

procedure TFServidorHTTP.ServidorCommandGet(AContext: TIdContext;
ARequestInfo: TIdHTTPRequestInfo; AResponseInfo: TIdHTTPResponseInfo);
var
sDocumento, sSesionID: String;
Sesion: TIdHTTPSession;
begin
Log.Lines.Add( ARequestInfo.RemoteIP + ': ' +
ARequestInfo.Command + ARequestInfo.Document );

// ¿Va a entrar a la página principal?
if ARequestInfo.Document = '/' then
AResponseInfo.ServeFile( AContext, ExtractFilePath( Application.ExeName ) + 'index.html' )
else
begin
// Cargamos la página web que vamos a enviar
sDocumento := ExtractFilePath( Application.ExeName ) +
Copy( ARequestInfo.Document, 2, Length( ARequestInfo.Document ) );

// ¿Existe la página que ha solicitado?
if FileExists( sDocumento ) then
begin
// validamos al usuario
if not ( ( ARequestInfo.AuthUsername = 'admin' ) and
( ARequestInfo.AuthPassword = '1234' ) ) then
AResponseInfo.AuthRealm := 'ServidorHTTP'
else
begin
// Componemos el ID de la sesión con el nombre del usuario y su password
sSesionID := ARequestInfo.RemoteIP + '_' + ARequestInfo.AuthUsername +
'_' + ARequestInfo.AuthPassword;

// Comprobamos si ese usuario ya tiene una sesión abierta
Sesion := Servidor.SessionList.GetSession( sSesionID, ARequestInfo.RemoteIP );

// Si no tiene sesión le pedimos autentificarse
if Sesion = nil then
// Creamos una nueva sesión para este usuario
Sesion := Servidor.SessionList.CreateSession( ARequestInfo.RemoteIP, sSesionID );

AResponseInfo.ServeFile( AContext, sDocumento );
end
end
else
// No hemos encontrado la página
AResponseInfo.ResponseNo := 404;
end;

AResponseInfo.CloseConnection := True;
end;

Después de identificar al usuario comprobamos si existe una sesión asociada al mismo. Cuando se crea una nueva sesión tenemos que darle un identificador que sea único en nuestro servidor. En nuestro caso he creado un ID juntando la IP del usuario, el nombre y su contraseña:

sSesionID := ARequestInfo.RemoteIP + '_' + ARequestInfo.AuthUsername +
'_' + ARequestInfo.AuthPassword;

Después compruebo si ya está abierta una sesión para este usuario y si no es así entonces le creamos una sesión:

// Comprobamos si ese usuario ya tiene una sesión abierta
Sesion := Servidor.SessionList.GetSession( sSesionID, ARequestInfo.RemoteIP );

// Si no tiene sesión le pedimos autentificarse
if Sesion = nil then
// Creamos una nueva sesión para este usuario
Sesion := Servidor.SessionList.CreateSession( ARequestInfo.RemoteIP, sSesionID );

Primero llama al método GetSession que necesita el identificador de la sesión y la IP del usuario remoto. Si no la encuentra creamos una nueva sesión que se añadirá automáticamente a la lista de sesiones del servidor.

Ahora introducimos en el evento OnSessionStart el código que guarda la fecha y la hora de cuando el usuario comenzó su conexión:

procedure TFServidorHTTP.ServidorSessionStart(Sender: TIdHTTPSession);
begin
Sender.Content.Text := DateTimeToStr( Now );
Log.Lines.Add( 'Iniciada sesion de ' + Sender.SessionID + ' en ' +
Sender.Content.Text );
end;

La clase TIdHTTPSession permite guardar en su variable Content (que es de la clase TStrings) cualquier texto que nos venga en gana. En mi caso sólo he guardado la fecha y hora de cuando entró el usuario.

En el evento OnSessionEnd mostramos en la ventana del servidor cuando finalizó el usuario:

procedure TFServidorHTTP.ServidorSessionEnd(Sender: TIdHTTPSession);
begin
Log.Lines.Add( 'Finalizada sesion de ' + Sender.SessionID + ' en ' +
DateTimeToStr( Now ) + ' (' + FormatFloat( '###0', MinuteSpan(
Now, StrToDateTime( Sender.Content.Text ) ) ) + ' minutos)' );
end;

La función MinuteSpan calcula la diferencia en minutos entre dos variables TDateTime. Para poder utilizar esta función hay que añadir arriba la unidad DateUtils.

Este sería el resultado al entrar a nuestro servidor:


Entramos en la zona privada:


Una vez dentro podemos esperar un par de minutos:


Al desactivar el servidor se cerrarán automáticamente todas las sesiones (lo hace sólo el componente TIdHTTPServer) mostrando en pantalla los minutos que ha permanecido nuestro usuario con la sesión abierta:


Mediante este sistema podemos guardar todas las acciones del usuario en el servidor sin necesidad de utilizar los cookies del navegador del cliente. Si queremos que los datos de cada usuario sean permanentes sólo hay que guardar el contenido de la variable Content a disco en un fichero cuyo nombre sea por ejemplo el ID del usuario. De ese modo, cuando el usuario se conecte otro día puede recuperar sus datos.

VARIACIONES PARA DELPHI 7

Como vimos en el artículo anterior, para Delphi 7 hay que hacer una pequeña variación ya que el objeto TIdHTTPResponseInfo no tiene el método ServeFile:

procedure TFServidorHTTP.ServidorCommandGet(AThread: TIdPeerThread;
ARequestInfo: TIdHTTPRequestInfo; AResponseInfo: TIdHTTPResponseInfo);
var
sDocumento, sSesionID: String;
Sesion: TIdHTTPSession;
S: TStringList;
begin
S := TStringList.Create;
Log.Lines.Add( ARequestInfo.RemoteIP + ': ' +
ARequestInfo.Command + ARequestInfo.Document );

// ¿Va a entrar a la página principal?
if ARequestInfo.Document = '/' then
begin
S.LoadFromFile( ExtractFilePath( Application.ExeName ) + 'index.html' );
AResponseInfo.ContentText := S.Text;
end
else
begin
// Cargamos la página web que vamos a enviar
sDocumento := ExtractFilePath( Application.ExeName ) +
Copy( ARequestInfo.Document, 2, Length( ARequestInfo.Document ) );

// ¿Existe la página que ha solicitado?
if FileExists( sDocumento ) then
begin
// validamos al usuario
if not ( ( ARequestInfo.AuthUsername = 'admin' ) and
( ARequestInfo.AuthPassword = '1234' ) ) then
AResponseInfo.AuthRealm := 'ServidorHTTP'
else
begin
// Componemos el ID de la sesión con el nombre del usuario y su password
sSesionID := ARequestInfo.RemoteIP + '_' + ARequestInfo.AuthUsername +
'_' + ARequestInfo.AuthPassword;

// Comprobamos si ese usuario ya tiene una sesión abierta
Sesion := Servidor.SessionList.GetSession( sSesionID, ARequestInfo.RemoteIP );

// Si no tiene sesión le pedimos autentificarse
if Sesion = nil then
// Creamos una nueva sesión para este usuario
Sesion := Servidor.SessionList.CreateSession( ARequestInfo.RemoteIP, sSesionID );

S.LoadFromFile( sDocumento );
AResponseInfo.ContentText := S.Text;
end
end
else
// No hemos encontrado la página
AResponseInfo.ResponseNo := 404;
end;

AResponseInfo.CloseConnection := True;
S.Free;
end;

Los demás métodos funcionan exactamente igual.

En el siguiente artículo vamos a seguir exprimiendo nuestro servidor con nuevas funcionalidades.

Pruebas realizadas en RAD Studio 2007 y Delphi 7.

17 octubre 2008

Crea tu propio servidor HTTP (1)

Si bien hoy en día hay servidores de páginas web gratuitos y de calidad como pueden ser Apache, LightHTTP, IIS, etc. no está de más crear nuestro propio servidor de páginas web para tener nuestra propia intranet en nuestro hogar o incluso en nuestra oficina. Si a eso le sumamos los conocimientos que ya tenemos de Delphi respecto al acceso a bases de datos podemos crear pequeñas aplicaciones web muy interesantes.

Como viene siendo habitual en esta serie de artículos vamos a utilizar los componentes Indy. Lo que vamos a hacer es que el usuario que se conecte al servidor entre a una página principal y luego para entrar a nuestra zona privada tenga que identificarse con usuario y contraseña.

CREANDO LA VENTANA DEL SERVIDOR

Al igual que hicimos con el servidor de Telnet, sería interesante monitorizar las conexiones de los usuarios que se van a conectar a nuestro servidor. Por ello vamos a crear un nuevo proyecto cuya ventana principal es la siguiente:

En este formulario hemos introducido dos botones para conectar y desconectar el servidor y un componente de la clase TMemo llamado Log donde iremos mostrando los eventos que ocurren. Y por último añadimos el componente más importante: IdHTTPServer situado en la pestaña Indy Servers.

El código asociado a los botones Activar y Desactivar no puede ser más sencillo:

procedure TFServidorHTTP.BActivarClick(Sender: TObject);
begin
Servidor.Active := True;
Log.Lines.Add( 'Servidor activado.' );
BActivar.Enabled := False;
BDesactivar.Enabled := True;
end;

procedure TFServidorHTTP.BDesactivarClick(Sender: TObject);
begin
Servidor.Active := False;
Log.Lines.Add( 'Servidor desactivado.' );
BActivar.Enabled := True;
BDesactivar.Enabled := False;
end;

Al ejecutar el programa y pulsar el botón Activar veremos que salta el cortafuegos de Windows (o el que tengamos configurado por defecto) donde debemos pulsar el botón Desbloquear para tener nuestro servidor operativo:


Después sólo tenemos que abrir nuestro navegador de Internet preferido y teclear localhost o bien http://127.0.0.1 y debe aparecer una página web en blanco.

Pues bien, cada vez que un usuario entra a nuestro servidor de páginas web con cualquier navegador, lo que realmente hace es enviarnos este comando por el puerto 80:

GET /

Esto significa que debemos darle la página principal de entrada, que en nuestro caso va a ser un archivo HTML que vamos a crear con el Bloc de Notas de Windows y que va a contener lo siguiente:


El archivo lo guardamos con el nombre index.html en el mismo directorio donde se encuentra el ejecutable del servidor.

Otra página que vamos a crear es la zona privada:


Este archivo lo guardamos con el nombre zonaprivada.html. Ahora viene el punto fuerte, donde hay que responder a los comandos del cliente utilizando el evento OnCommandGet del componente IdHTTPServer:

procedure TFServidorHTTP.ServidorCommandGet(AContext: TIdContext;
ARequestInfo: TIdHTTPRequestInfo; AResponseInfo: TIdHTTPResponseInfo);
var
sDocumento: String;
begin
Log.Lines.Add( ARequestInfo.RemoteIP + ': ' +
ARequestInfo.Command + ARequestInfo.Document );

// ¿Va a entrar a la página principal?
if ARequestInfo.Document = '/' then
AResponseInfo.ServeFile( AContext, ExtractFilePath( Application.ExeName ) + 'index.html' )
else
begin
// Cargamos la página web que vamos a enviar
sDocumento := ExtractFilePath( Application.ExeName ) +
Copy( ARequestInfo.Document, 2, Length( ARequestInfo.Document ) );

// ¿Existe la página que ha solicitado?
if FileExists( sDocumento ) then
begin
// validamos al usuario
if not ( ( ARequestInfo.AuthUsername = 'admin' ) and
( ARequestInfo.AuthPassword = '1234' ) ) then
AResponseInfo.AuthRealm := 'ServidorHTTP'
else
AResponseInfo.ServeFile( AContext, sDocumento );
end
else
// No hemos encontrado la página
AResponseInfo.ResponseNo := 404;
end;

AResponseInfo.CloseConnection := True;
end;

Para poder compilar este ejemplo hay que añadir arriba la unidad IdContext. Este evento se divide en las siguientes partes:

1º Escribimos en la ventana de nuestro servidor quien se está conectando y el documento (pagina web, archivo, etc.) solicita:

Log.Lines.Add( ARequestInfo.RemoteIP + ': ' +
ARequestInfo.Command + ARequestInfo.Document );

2º Si lo que el usuario solicita es la página principal pues entonces se la damos sin rechistar:

// ¿Va a entrar a la página principal?
if ARequestInfo.Document = '/' then
AResponseInfo.ServeFile( AContext, ExtractFilePath( Application.ExeName ) + 'index.html' )
else
...


El método ServeFile envía cualquier archivo al navegador del cliente.

3º Traducimos el documento que nos solicitan en una ruta donde estamos ejecutando el servidor:

// Cargamos la página web que vamos a enviar
sDocumento := ExtractFilePath( Application.ExeName ) +
Copy( ARequestInfo.Document, 2, Length( ARequestInfo.Document ) );

4º Si el documento que nos solicitan no existe le mandamos un error 404. En el caso de que exista, significa que va a entrar en nuestra zona privada, con lo cual compruebo primero su usuario y password antes de dejarle pasar:

// ¿Existe la página que ha solicitado?
if FileExists( sDocumento ) then
begin
// validamos al usuario
if not ( ( ARequestInfo.AuthUsername = 'admin' ) and
( ARequestInfo.AuthPassword = '1234' ) ) then
AResponseInfo.AuthRealm := 'ServidorHTTP'
else
AResponseInfo.ServeFile( AContext, sDocumento );
end
else
// No hemos encontrado la página
AResponseInfo.ResponseNo := 404;

Una vez autentificado el usuario le enviamos la página web solicitada.

5º Por último cerramos la conexión con el cliente:

AResponseInfo.CloseConnection := True;

Ahora ejecutamos el programa y pulsamos el botón Conectar:


Ejecutamos por ejemplo Internet Explorer y escribimos locahost:

Al pulsar el enlace Ir a la zona privada hará que el navegador nos pida usuario y contraseña:


Escribimos admin Y 1234 y saltará a la zona privada:


Si volvemos a ir a la página principal y e intentamos entrar en la zona privada ya no será necesario introducir usuario y contraseña, ya que lo memoriza automáticamente el navegador.

Si intentamos solicitar a nuestro servidor una página que no existe nos devolverá un error 404:


Mientras ha sucedido todo esto, nuestro servidor ha monitorizado todo el proceso:


En esto hay que ver tres cosas importantes:

1º Si el usuario vuelve a la página principal index.html el servidor no se entera, ya que el navegador web la ha cogido de su caché de páginas, evitando el tráfico innecesario.

2º Cuando nos conectamos por primera vez a una página web, el navegador solicita el archivo favicon.ico. Este icono es el que aparece al lado de la URL de la página web donde hemos entrado:


Si os interesa más información sobre el icono favicon podéis verlo en esta página:

http://www.favicon.net/

De todas formas no hay que tomarle mucha importancia a esto porque por ejemplo Firefox no lo pide.

3º El control de entrada de usuarios que hemos realizado sólo es eso, control de usuarios, pero no es una sesión. Una sesión es la encargada de guardar el estado del usuario en nuestra página. Eso lo veremos más adelante.

CONCLUSIÓN

Como puede apreciarse en el código, con algo tan sencillo como esto ya tenemos un pequeño servidor web que permite servir páginas web a los clientes y tener una zona privada que podíamos validar con una pequeña base de datos de Internase o bien un pequeño archivo de texto encriptado.

Pero no hay que hacerse muchas ilusiones ya que un servidor web incluye muchas más cosas. Una cosa es dejar que un usuario entre a una zona privada y otra guardar el estado del usuario con una sesión mediante cookies. Eso lo veremos en el siguiente artículo. También sería interesante poder recoger información de usuarios y controlar eventos al estilo web 2.0, como puede ser la implementación de un blog, un foro, un chat online, etc.

MODIFICACIONES QUE HAY QUE REALIZAR PARA DELPHI 7

El único inconveniente que tiene este código en Delphi 7 (cuya versión de los componentes Indy es más antigua) es que el objeto AResponseInfo no tiene el método ServeFile por lo que tenemos que dárle la página web a mano. Lo que he realizado en este ejemplo es cargar la página web en un StringList y se la doy al cliente:

procedure TFServidorHTTP.ServidorCommandGet(AThread: TIdPeerThread;
ARequestInfo: TIdHTTPRequestInfo; AResponseInfo: TIdHTTPResponseInfo);
var
sDocumento: String;
S: TStringList;
begin
S := TStringList.Create;
Log.Lines.Add( ARequestInfo.RemoteIP + ': ' +
ARequestInfo.Command + ARequestInfo.Document );

// ¿Va a entrar a la página principal?
if ARequestInfo.Document = '/' then
begin
S.LoadFromFile( ExtractFilePath( Application.ExeName ) + 'index.html' );
AResponseInfo.ContentText := S.Text;
end
else
begin
// Cargamos la página web que vamos a enviar
sDocumento := ExtractFilePath( Application.ExeName ) +
Copy( ARequestInfo.Document, 2, Length( ARequestInfo.Document ) );

// ¿Existe la página que ha solicitado?
if FileExists( sDocumento ) then
begin
// validamos al usuario
if not ( ( ARequestInfo.AuthUsername = 'admin' ) and
( ARequestInfo.AuthPassword = '1234' ) ) then
AResponseInfo.AuthRealm := 'ServidorHTTP'
else
begin
S.LoadFromFile( sDocumento );
AResponseInfo.ContentText := S.Text;
end
end
else
AResponseInfo.ResponseNo := 404;
end;

AResponseInfo.CloseConnection := True;
S.Free;
end;

Igualmente se podía haber cargado el archivo con AssignFile y Reset o bien con un objeto TStreamFile. Eso lo dejo al gusto del usuario.

En el próximo artículo veremos como sacarle más partido a este componente.

Pruebas realizadas en RAD Studio 2007 y Delphi 7.

10 octubre 2008

Creando un cliente de Telnet

Si en artículo anterior vimos como crear un servidor de Telnet, teníamos el inconveniente de utilizar como cliente el programa Telnet.exe que trae por defecto Windows. Aunque hay muchos clientes de Telnet tanto de pago como libres vamos a ver como crear el nuestro propio.

CREANDO LA VENTANA DEL CLIENTE

Creamos un nuevo proyecto cuyo formulario principal se compone de estos componentes:


Contiene dos componentes de la clase TEdit llamados IPServidor y Puerto, así como un botón llamado BConectar. Insertaremos un componente TMemo llamado Log el cual vamos a utilizar para mostrar el texto del terminal al conectarse y el componente TIdTelnet que llamaremos Telnet y que se encuentra en la pestaña de componentes Indy Clients.

CONECTANDO CON EL SERVIDOR

Al pulsar el botón Conectar incluimos este código:

procedure TFClienteTelnet.BConectarClick(Sender: TObject);
begin
Telnet.Host := IPServidor.Text;
Telnet.Port := StrToInt( Puerto.Text );
Telnet.Connect;
Log.SetFocus;
end;

Es tan sencillo como indicarle la IP del servidor, el puerto y conectar. Después de conectarse mandamos el foco al memo para que el usuario pueda escribir en el terminal.

En el evento OnConnect del componente Telnet confirmamos la conexión por pantalla:

procedure TFClienteTelnet.TelnetConnect(Sender: TObject);
begin
Log.Lines.Add( 'Conectando con ' + Telnet.Host + '.' );
Log.Lines.Add( '' );
end;

RECIBIENDO LA INFORMACIÓN DEL SERVIDOR

Una vez conectados, es el servidor el que nos manda el texto del terminal a través del evento OnDataAvailable:

procedure TFClienteTelnet.TelnetDataAvailable(Sender: TIdTelnet;
const Buffer: string);
begin
Log.Text := Log.Text + Buffer;
Log.SelStart := Length( Log.Text );
end;

Cuando el servidor nos mande el texto lo introducimos en el campo memo y situamos el cursor en la última línea para que el usuario pueda seguir escribiendo.

PROCESANDO LOS COMANDOS

Para procesar los comandos que vamos a escribir en nuestro terminal vamos a crear una variable privada a nivel de formulario llamada sComando:

...
private
{ Private declarations }
sComando: String;
...

Y en el evento OnKeyPress del campo memo llamado Log hacemos lo siguiente:

procedure TFClienteTelnet.LogKeyPress(Sender: TObject; var Key: Char);
begin
if not Telnet.Connected then
Exit;

if Key = #13 then
begin
Telnet.Socket.WriteLn( sComando );
sComando := '';
end
else
sComando := sComando + Key;
end;

Si el usuario no ha pulsado la tecla Intro vamos añadiendo letras al comando hasta que la pulse. Entonces enviamos el comando al servidor y volvemos a vaciar la variable sComando para la siguiente instrucción.

PROBANDO LA CONEXIÓN

Pues con sólo lo que hemos hecho ya tenemos un minicliente de Telnet que vamos a probar en el mismo PC con el servidor que creamos en el artículo anterior:


Como podeis apreciar en la imagen (hacer clic para ampliar) cuando introducimos el usuario admin y la clave 1234 el servidor de telnet nos devuelve un eco con lo tecleado, aunque luego una vez dentro se comporta de manera normal.

Con un par de programas como este bien realizados se puede crear un servicio de teletexto entre varias empresas que funciona a una velocidad mucho más rápida que mediante páginas web.

Pruebas realizadas en RAD Studio 2007.

03 octubre 2008

Monitorizando aplicaciones con Telnet

El protocolo de comunicación Telnet (TELecommunication NETwork) fue diseñado inicialmente para acceder a una máquina remota por medio de comandos de teclado a través de una consola virtual utilizando para ello el puerto 23. Fue muy utilizado en los comienzos de Internet para administrar servidores Unix desde terminales remotos.

Aunque es un protocolo algo obsoleto (comparado con monitorizar ordenadores con programas como VNC o Terminal Server) si que podemos sacarle partido para controlar el estado de nuestras aplicaciones a través de Internet accediendo a la ordenador de nuestro cliente.

CREANDO EL SERVIDOR DE TELNET

Para crear un servidor de Telnet vamos a utilizar el componente IdTelnetServer situado en la pestaña de componentes Indy. Sólo tenemos que crear un nuevo formulario e introducir dicho componente:


Insertamos un componente de la clase TMemo que llamaremos Log que vamos a utilizar para monitorizar la conexión con el cliente. Luego añadimos el componente IdTelnetServer y los llamamos Servidor.

En este componente tenemos que dejar activado el servidor poniendo la propiedad Active a True. En el campo DefaultPort viene por defecto el 23 aunque podemos poner cualquier otro evitar evidencias por si hay alguién por la red escaneando puertos abiertos por debajo del 1024.

Lo que hay que asegurarse es que dicho puerto este habilitado por nuestro software que hace de cortafuegos y si tenemos un router habrá que redireccionar ese puerto desde nuestra IP pública a la tarjeta de red por medio de las opciones NAT.

Para ver si todo funciona bien ejecutamos el programa y saltará el cortafuegos de Windows (o el que tengamos por defecto) para habilitar el servidor:


Para poder probar si funciona nuestro servidor necesitamos un programa que haga de cliente de Telnet. Afortunadamente, Windows dispone en la consola de el cliente telnet.exe. Abrimos una ventana de comandos (Símbolo del sistema) y hacemos una conexión local escribiendo telnet 127.0.0.1. Si todo marcha según lo previsto debe verse este mensaje:


Podemos escribir cualquier cosa pulsando luego Intro y no hará absolutamente nada. Quedará como si estuviésemos escribiendo en un editor de texto. Al cerrar la ventana de nuestro servidor, la ventana de comandos nos dirá que hemos perdido la conexión:


También disponemos del programa Hyperterminal que se encuentra en Inicio -> Programas -> Accesorios -> Herramientas del sistema -> Comunicaciones -> Hyperterminal. Aunque este programa da algunos problemas que luego comentaré más adelante. Si no lo tuviésemos instalado se puede hacer fácilmente a través de la opción Agregar o quitar programas que se encuentra en el Panel de control.

De todas formas, con el cliente telnet de la línea de comandos nos sobra para hacer las pruebas pertinentes.

CONTROLANDO LA CONEXIÓN CON EL SERVIDOR

Para controlar el estado de la conexión vamos a utilizar los eventos OnConnect y OnDisconnet para informar en la ventana del servidor quien se ha conectado:

procedure TFServidorTelnet.ServidorConnect(AContext: TIdContext);
begin
Log.Lines.Add( 'Se ha conectado: ' + AContext.Binding.PeerIP );
end;

procedure TFServidorTelnet.ServidorDisconnect(AContext: TIdContext);
begin
Log.Lines.Add( 'Se ha desconectado: ' + AContext.Binding.PeerIP );
end;

Ahora pasemos a la validación del usuario.

VALIDANDO LOS USUARIOS QUE SE CONECTAN

Lo primero que vamos a hacer es cambiar el mensaje de bienvenida poniendo en la propiedad LoginMessage del componente IdTelnetServer la frase Servidor de Telnet.

Antes de proceder a leer comandos del cliente primero tenemos que autentificarlo para evitar intrusiones de terceros. Esto se hace en su evento OnAuthentitacion:

procedure TFServidorTelnet.ServidorAuthentication(AContext: TIdContext;
const AUsername, APassword: string; var AAuthenticated: Boolean);
begin
Log.Lines.Add('Usuario: '+AUserName);
Log.Lines.Add('Password: '+APassword);

if (AUsername = 'admin') and (APassword = '1234') then
begin
AAuthenticated := True;
AContext.Connection.Socket.WriteLn( 'Bienvenido al servidor de Telnet.' );
AContext.Connection.Socket.Write( '' );
AContext.Connection.Socket.Write( '>' );
Log.Lines.Add( 'Acceso concedido.' );
end
else
begin
AAuthenticated := True;
AContext.Connection.Socket.WriteLn( 'Acceso denegado.' );
AContext.Connection.Disconnect;
Log.Lines.Add( 'Acceso denegado.' );
end;
end;

Para poder compilar sin problemas con este evento debemos añadir arriba la unidad IdContext.

Es este evento lo primero que hacemos es imprimir en la ventana del servidor el usuario y password del que se conecta. Luego si adivina el usuario admin y la clave 1234 entonces le damos el mensaje de bienvenida y esperamos a que el usuario escriba un comando. Si no ha podido acceder le denegamos la conexión.

LEYENDO LOS COMANDOS ENVIADOS POR EL CLIENTE TELNET

Una vez que el usuario esta dentro lo que hay que hacer es ir leyendo los comando que envía y según nuestro criterio le vamos dando respuestas. Esto se hace en el evento OnExecute:

procedure TFServidorTelnet.ServidorExecute(AContext: TIdContext);
var
sComando: String;
begin
sComando := LowerCase( AContext.Connection.Socket.ReadLn );
Log.Lines.Add( 'Comando: ' + sComando );

if sComando = 'quit' then
begin
AContext.Connection.Socket.WriteLn( 'Gracias por su visita.' );
AContext.Connection.Disconnect;
end
else
if sComando = 'fecha' then
begin
AContext.Connection.Socket.WriteLn( DateToStr( Date ) );
AContext.Connection.Socket.Write( '>' );
end
else
if sComando = 'hora' then
begin
AContext.Connection.Socket.WriteLn( TimeToStr( Time ) );
AContext.Connection.Socket.Write( '>' );
end
else
begin
AContext.Connection.Socket.WriteLn( 'Comando desconocido: '+sComando );
AContext.Connection.Socket.Write( '>' );
end;
end;

Este es un proceso cíclico en el que vamos leyendo comandos y dando su respuesta. En este caso sólo he programado tres comandos:

quit -> abandona la conexión

fecha -> devuelve la fecha del ordenador remoto

hora -> devuelve la hora del ordenador remoto


PROBANDO EL SERVIDOR


Vamos a realizar una prueba de conexión:


Esta sería la ventana de bienvenida:


Ahora escribimos como usuario admin con password 1234:


Una cosa rara que pasa con nuestro servidor telnet de Indy es que envía el eco de lo que escribimos, cuyo problema no se si esta en el cliente telnet.exe o en el componente IdTelnetServer. Cuando lo averigüe actualizaré este artículo con la solución (se aceptan sugerencias). Con el programa Hyperterminal no sucede esto al hacer login, pero tiene el problema que una vez estamos dentro no vemos lo que estamos escribiendo.

En fin, escribimos usuario y clave y al entrar veremos el mensaje de bienvenida:


Ahora probamos cada uno de nuestros comandos:


En nuestro servidor también vamos registrando los eventos:


Como se puede apreciar en la ventana del servidor vemos las acciones que ha ido haciendo nuestro cliente. Este protocolo tiene la ventaja de que gasta un ancho de banda ridículo para los tiempos que estamos (ADSL, Cable, etc.) dándonos la potencia de monitorizar varios ordenadores simultáneamente.

Podemos inventarnos todos los comandos que nos vengan en gana, por ejemplo:

- Leer las unidades de disco del ordenador remoto.

- Acceder a las tablas de una base de datos y ampliar o reducir campos.

- Ejecutar eventos en el PC remoto: abrir CD-ROM, apagar equipo, ejecutar copia de seguridad, capturar la pantalla y enviarla por correo, etc.

Es decir, se pueden crear todas las funciones de un troyano (no seáis malos). El único inconveniente que tiene este protocolo es que viaja por la red sin encriptar, con lo que si alguien un poco avispado intercepta nuestra conexión con un sniffer podría averiguar el usuario y password de nuestro servidor telnet.

En el próximo artículo veremos las alternativas que tenemos para solucionar este problema.

Pruebas realizadas en RAD Studio 2007.

Publicidad