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.
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.
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.
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.
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; } |
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; } |
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 |