3. Ejemplo de demonio: db-httpd

La programación con inetd es sencilla: lo que leamos de la entrada estándar corresponderá con lo que nos envían los clientes y lo que escribamos en en la salida estándar será lo que se envíe desde el servidor.

Esta sencillez nos permitirá realizar servidores con lenguajes de programación que, sin disponer de funciones para trabajar en red, sí disponen mecanismos para manipular la entrada y la salida estándar.

En este caso vamos a realizar un pequeño servidor de páginas web un tanto especial. Nuestro db-httpd será capaz de servir recursos mediante el protocolo HTTP, pero estos recursos se encontrarán en una base de datos y no el sistema de ficheros como es más habitual.

Además se ha implementado en unas 80 líneas de código shell, junto a un sencillo interface en C para la base de datos.

3.1. Diseño del demonio

La mayor parte del trabajo realizado por el servidor HTTP se lleva a cabo empleando herramientas disponibles en cualquier entorno de shell.

Las herramientas empleadas son: date(1), echo(1), test(1), printf(1), sed(1) y tr(1).

La interfaz para la bases de datos es simple. Se trata de un programa que dada una clave se encarga de devolver el recurso asociado para una base de datos concreta.

Para aumentar la funcionalidad del servidor se han definido dos bases de datos: httpd-index y httpd-db.

La primera base de datos se encarga de indexar los recursos asociando pares CAMINO/RECURSO, donde RECURSO puede ser el camino a un fichero en disco u otra clave con la que obtener el recurso propiamente dicho de la segunda base de datos.

De esta forma podemos servir páginas directamente desde disco o desde la base de datos, empleando unicamente llamadas al programa auxiliar.

3.2. Implementación

Para la implementación se ha empleado la shell ksh para el servidor HTTP y C para las herramientas que manejan la base de datos.

Como backend de la base de datos se ha elegido ndbm por su sencillez y disponibilidad, aunque podría emplear algún sistema de gestión de bases de datos como MySQL respetando el sencillo interface sin ninguna dificultad.

3.2.1. NDBM

Se han implementado dos herramientas para manipular las bases de datos con ndbm(3), aunque solo una de ellas será invocada por el servidor, quedando la otra relegada a tareas de mantenimiendo, como generar las bases de datos.

3.2.1.1. mkhttpd-db

Esta es la herramienta administrativa que se encarga de leer de la entrada estándar los pares CAMINO/RECURSO para generar las bases de datos httpd-index.db y httpd-db.db.

Su funcionamiento es sencillo: se introduce en la base de datos httpd-index.db el valor RECURSO empleando como clave CAMINO. Si RECURSO comienza por db:, entonces se emplea RECURSO como clave para almacenar el fichero completo en la base de datos httpd-db.db.

Se exige que el fichero exista si debe alojarse en la base de datos, sino no es necesario.

Por ejemplo, si disponemos un fichero llamado httpd-index con el siguiente contenido:
/ db:/var/www/index.html
/index.html db:/var/www/index.html
/imagen.jpg /var/www/imagen.jpg

Y ejecutamos:
# mkhttpd-db < httpd-index
httpd-index.db: 3 line(s) processed

Tendremos en httpd-index.db entradas para /, /index.html e /index.jpg, con las tres primeras apuntando cada una a su recurso correspondiente en index-db.db y con la última entrada apuntando al fichero en disco /var/www/imagen.jpg.

El código de esta herramienta puede parecer algo complicado debido a que se hace control de errores:
#include<fcntl.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<unistd.h>
#include<stdio.h>
#include<string.h>
#include<stdlib.h>

#include"ndbm.h"

int
main(int argc, char *argv[])
{
	char 	idx[1024],
		file[1024];

	FILE	*fd;
	char	*buffer;
	struct stat fs;
	
	DBM 	*db_idx,
		*db_db;
	datum 	key,
		data;

	int 	line;

	db_idx=dbm_open(SERVER_INDEX, O_CREAT | O_TRUNC | O_RDWR, 0664);
	if(!db_idx)
	{
		perror(argv[0]);
		return 1;
	}
	
	db_db=dbm_open(SERVER_DB, O_CREAT | O_TRUNC | O_RDWR, 0664);
	if(!db_db)
	{
		perror(argv[0]);
		return 1;
	}
	
	line=1;
	while(!feof(stdin))
	{
		if(fscanf(stdin,"%1024[^ ] %1024[^\n]\n", idx, file)==2)
		{
			key.dptr=idx;
			key.dsize=strlen(idx);

			data.dptr=file;
			data.dsize=strlen(file)+1;
				
			dbm_store(db_idx, key, data, DBM_REPLACE);

			if(!strncmp(file,"db:",3))
			{
				if(strlen(file)>3) {
					if(stat(file+3,&fs)!=-1)
					{
						buffer=(char *)malloc(fs.st_size);
						if(buffer)
						{
							fd=fopen(file+3,"r");
							if(fd)
							{
								fread(buffer,fs.st_size,1,fd);
								data.dptr=buffer;
								data.dsize=fs.st_size;

								key.dptr=file;
								key.dsize=strlen(file);

								dbm_store(db_db, key, data, DBM_REPLACE);

								fclose(fd);
							} else
								fprintf(stderr,"%s: failed to add file arround line %i\n", argv[0], line);


							free(buffer);
						} else
							fprintf(stderr,"%s: failed to add file arround line %i\n", argv[0], line);
					} else
						fprintf(stderr,"%s: file not found arround line %i\n", argv[0], line);
				} else
					fprintf(stderr,"%s: null file arround line %i\n", argv[0], line);
			}
		} else
			fprintf(stderr,"%s: error arround line %i\n",
					argv[0], line);
		line++;
	}

	printf("%s.db: %i line(s) processed\n", SERVER_INDEX, line-1);
	
	dbm_close(db_idx);
	dbm_close(db_db);

	return 0;
}

3.2.1.2. httpd-fetch

Esta es la herramienta que llamará el servidor para acceder a los elementos almacenados en la bases de datos.

Su funcinamiento es el siguiente: requiere de 2 argumentos, el nombre de la base de datos (sin la extensión .db) y la clave del recurso a obtener.

Si la clave se encuentra en la base de datos, el programa devuelve 0 y escribe en la salida estándar el recurso asociado. En caso de no encontrar la clave, el programa devuelve un 1.

Con este comportamiento tan sencillo el servidor se encarga de hacer una primera llamada preguntando por el CAMINO en la base de datos httpd-index. Si se devuelve un 1 el servidor muestra un error HTTP 404.

Si el CAMINO está en la base de datos comprueba si el recurso devuelto comienza por db:. En caso negativo simplemente se envía el fichero apuntado por ese recurso (en caso de existir en disco, sino se devuelve un error HTTP 404). Si por el contrario el recurso comienza por db:, el servidor realiza otra llamada a httpd-fetch, esta vez empleando la base de datos httpd-db como primer argumento y el recurso obtenido de la llamada anterior como clave para la búsqueda. El resultado de esta petición se envía directamente al cliente como respuesta.

El código de esta herramienta es muy simple, pese a la gran funcionalidad que aporta al servidor:
#include<fcntl.h>
#include<stdio.h>

#include"ndbm.h"

int
main(int argc, char *argv[])
{
	DBM 	*db;
	datum 	key,
		data;

	if(argc<3)
	{
		printf("%s db key\n", argv[0]);
		return 1;
	}

	db=dbm_open(argv[1], O_RDONLY, 0);
	if(!db)
	{
		printf("%s: failed to fetch from %s.db\n", argv[0], argv[1]);
		return 1;
	}

	key.dptr=argv[2];
	key.dsize=strlen(argv[2]);
        data=dbm_fetch(db, key);	

	if(data.dptr)
		fwrite(data.dptr,data.dsize,1,stdout);

	dbm_close(db);
	return 0;
}

3.2.2. db-httpd

Esta es la parte principal del servidor. Solo procesa peticiones GET e implementa un mínimo del protocolo.

Se ha extraido del fuente la identificación del tipo MIME de cada recurso al fichero mime.types por comodidad, aunque no sería necesario.

Ya que este es el servidor propiamente dicho, merece la pena estudiarlo linea a linea:
#!/bin/sh

# variables de configuración
# se generan en la instalación, no tocar
LANG=en_EN
SERVER_NAME="db-http"
SERVER_URL="http://www.usebox.net/jjm/db-httpd/"
SERVER_FETCH="/usr/local/libexec/db-httpd/httpd-fetch"
SERVER_INDEX="/usr/local/etc/db-httpd/httpd-index"
SERVER_DB="/usr/local/etc/db-httpd/httpd-db"
SERVER_MIME="/usr/local/etc/db-httpd/mime.types"

#
# funcion auxiliar que envía las cabeceras HTTP
#
# entrada: ESTADO DESCRIPCION_CORTA TIPO_MIME
#
function send_headers
{
	NOW=`date +"%a, %d %b %Y %H:%M:%S GMT"`
	
	echo -e "HTTP/1.0 $1 $2\r"
	echo -e "Server: $SERVER_NAME\r"
	echo -e "Date: $NOW\r"

	if [ $3 ]; then
		echo -e "Content-Type: $3\r"
	fi
	
	echo -e "Connection: close\r\n\r"
}

#
# funcion auxiliar que envia una página HTML de error
#
# entrada: ESTADO DESCRIPCION_CORTA DESCRIPCION_LARGA
#
function send_error
{
	send_headers "$1" "$2" "text/html"

	echo -e "<html>\n<head><title>HTTP ERROR $1</title></head>\n"
	echo -e "<body bgcolor=#f0e0a0>\n<h2>$1 - $2</h2>\n<p><b>$3</b>\n"
	echo -e "<hr>\n<small>$SERVER_NAME, $SERVER_URL</small>\n</body>\n"
	echo -e "</html>\n"
}

# *** Comienza la ejecución ***

# leemos 3 cadenas: método, el recurso que nos piden y el protocolo
read METHOD RESOURCE_INDEX PROTOCOL

# es necesario leer el resto de la petición, aunque no la usamos para nada
ENDL=`printf "\r\n"`
read DUMMY
while [ $? = 0 -a "$DUMMY" != "" -a "$DUMMY" != "$ENDL" ]; do
	read DUMMY
done

# si el método no es 'get' devolvemos una página de error
METHOD=`echo -n $METHOD | tr [:upper:] [:lower:]` 
if [ "$METHOD" != "get" ]; then
	send_error "501" "Not implemented" "That method is not implemented."
	exit 1
fi

# primera llamada a base de datos
RESOURCE_PATH=`$SERVER_FETCH $SERVER_INDEX $RESOURCE_INDEX` 

# si no se puede consultar la base de datos,
# mostramos una página de error
if [ $? = 1 ]; then
	send_error "500" "Internal error" "$RESOURCE_PATH"
	exit 1
fi

# incluimos el fuente que procesa el recurso para obtener
# el tipo MIME - el tipo mime se almacena en la variable MIME
. $SERVER_MIME

# si el recurso comienza por 'db:', se realiza la segunda llamada
# a base de datos devolviendo al cliente el recusto obtenido esta vez
if [ "yes" = "`echo "$RESOURCE_PATH" | sed 's/db:.*/yes/'`" ]; then
	send_headers "200" "OK" $MIME
	$SERVER_FETCH $SERVER_DB $RESOURCE_PATH

	exit 0
fi

# si no es un recurso almacenado, enviamos al cliente el fichero
# correspondiente si es que existe
if [ -f "$RESOURCE_PATH" ]; then
	send_headers "200" "OK" $MIME

	cat $RESOURCE_PATH
	exit 0
fi

# sino existe, mostramos una página de error
send_error "404" "Not found" "The requested resource is not available."

exit 1

# EOF

El fichero mime.types:
case "$RESOURCE_PATH" in

*.html | *.htm)		MIME="text/html";;
*.jpg | *.jpeg | *.jpe)	MIME="image/jpeg";;
*.gif)			MIME="image/gif";;
*.png)			MIME="image/png";;

*)			MIME="text/plain";;

esac

# EOF