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.

Publicidad