Esta semana tuve que desarrollar una solución para descargar un backup fresco a través de HTTP. Se trata de una de esas manganetas que lamentablemente uno tiene que implementar, sobre uno de esos sistemas que uno quisiera no administrar, para esos clientes que uno quisiera no tener. Sin embargo, cuando uno tiene una mentalidad positiva cuenta con la certeza de que de todo lo malo se puede sacar algo bueno, como por ejemplo: aprender algo nuevo.

Con el perdón por esta breve introducción catárquica, ésta fue una buena oportunidad para experimentar con Bash en modo CGI y Apache. Es decir, correr desde Apache un script Bash que genere una respuesta HTTP. El resultado es que al acceder a determinada URL, se ejecuta un script Bash en el servidor. Lo mismo que hace cualquier aplicación CGI con otros lenguajes como PHP, Python, Perl, Ruby, etc. La cuestión es que (más allá de que cuento con suficiente experiencia desarrollando script en PHP, Python y Perl) necesitaba resolver este requerimiento de manera urgente. Y como mi lenguaje predilecto, y con el que iba a resolver esto lo más rápido posible (como 30 segundos o algo así) es Bash, me dispuse a configurar Apache para correr el script en modo CGI.

Los 30 segundos son literales, porque ¿cuánto tiempo puede llevar crear un script que tome unos archivos y cree un ZIP? Estamos hablando de una (1) línea de código.

Manos a la obra

Cierta aplicación de código cerrado genera una exportación de datos en decenas de archivos de texto plano con extensión .txt. El cliente requería poder descargar vía Web la copia más reciente de estos archivos de texto, los cuales se van sobrescribiendo pero no tienen nombres predecibles ni tampoco se puede conocer con exactitud en qué momento se generan (sólo el usuario lo sabe en el momento en que dispara un evento en la aplicación).

El problema es que, al no poder predecir los nombres de archivo, el momento en que se generan, ni tampoco la cantidad de archivos, no era posible implementar una solución con inotifywait para crear el archivo ZIP de manera automática. La limitación más grande era que no se sabía la cantidad de archivos ni cuál era el último (para poder crear el ZIP recién una vez haya sido escrito en disco el último de ellos).

Por supuesto el usuario no quería descargar uno por uno todos los archivos (tarea tediosa y propensa a errores).

La única solución era utilizar un script CGI que cree el ZIP al momento de acceder a cierta URL (el servidor contaba con Apache ya instalado y prestando servicios para otra aplicación). Lo que hubiera hecho cualquier dev/devops seguramente sería crear un script en PHP/Python/Perl, pero entre que buscaba la librería a utilizar y demás, me lleva más tiempo que escribir:

zip export.zip *.txt

Seguramente la solución utilizando cualquiera de los mencionados lenguajes sea 3 líneas de código o menos, la cuestión era el tiempo que me llevaría investigar y hacer pruebas.

Ahora sí, manos a la obra

La aplicación crea los archivos txt correspondiente a la exportación de datos en el directorio /home/export/. Mi objetivo era que, al ingresar el usuario al recurso "/export/backup.zip", no acceda directamente a un archivo ZIP previamente creado, sino que se ejecute el script Bash para crearlo en ese preciso momento.

A tal fin, el primer paso consiste en crear un directorio llamado "backup.zip" y dar permisos de lectura para todo el mundo, a fin de que Apache pueda acceder y ejecutar el script:

# mkdir -p /home/export/backup.zip
# chmod -R 755 /home/export

Es posible crear el directorio "backup.zip" en cualquier otra ubicación del sistema de archivos, sin embargo siempre será necesario que Apache pueda acceder al directorio "/home/export" a fin de leer los archivos txt de la exportación.

Luego crear el script Bash que se encargará de generar el ZIP:

# nano /home/export/backup.zip/backup.sh

Este script tiene el siguiente contenido:

#!/bin/sh

cd /home/export/

zip /tmp/export.temp.zip *.txt >/dev/null 2>/dev/null

echo Content-type: application/zip
echo "Content-Disposition: attachment; filename=backup.zip"
echo ""

/bin/cat /tmp/export.temp.zip

/bin/rm /tmp/export.temp.zip

Luego del hashbang, las dos primeras líneas cambian al directorio de exportación y generan el ZIP en el directorio temporal del sistema. Las restantes líneas se encargan de la respuesta HTTP. A través de la cabecera HTTP se indica que se trata de una respuesta de tipo ZIP, y el nombre del archivo es "backup.zip" en forma de adjunto (esto hace que se abra el cuadro de diálogo de descarga de archivo en el navegador del cliente).

Luego se vuelca el contenido del ZIP con cat y se elimina el ZIP temporal. Piece of cake.

Ahora resta configurar Apache para que se ejecute automáticamente el script /home/export/backup.zip/backup.sh cada vez que se acceda al recurso "/export/backup.zip". Y es aquí donde viene la manganeta.

Para este fin utilizo el VirtualHost por defecto:

# nano /etc/apache2/sites-enabled/000-default

Primero se debe configurar el siguiente alias en el servidor (dentro de la sección <VirtualHost>):


        Alias "/export" "/home/export"
        <Location "/home/export">
                Options +Indexes +FollowSymLinks +MultiViews
                AllowOverride None
                Order allow,deny
                allow from all
        </Location>

Este alias permite que se pueda listar vía Web el contenido del directorio /home/export. Un requisito del cliente para tener la capacidad de descargar cualquiera de los txt de manera individual.

Ahora viene la parte interesante: configurar Apache para que al acceder al recurso "/export/backup.zip", en lugar de listar su contenido o responder "403 Forbidden" se ejecute el script backup.sh (ejecutar, no volcar el contenido del script como un archivo cualquiera) y se vuelque su salida. Agregar las siguientes líneas a continuación de las anteriores:


        <Directory "/home/export/backup.zip">
                Options +Indexes +ExecCGI
                DirectoryIndex backup.sh
                AddHandler cgi-script sh
        </Directory>

Dentro de la configuración para el directorio "/home/export/backup.zip", la primera línea habilita la ejecución de scripts CGI (+ExecCGI). Luego se indica que el índice para el directorio es el archivo backup.sh (en lugar de index.html o como lo es por defecto), esto evita que se liste el directorio si está presente dicho archivo. Por último se indica a Apache que todos los archivos con extensión .sh sean ejecutados como scripts CGI por el módulo mod_cgi.

Con esto se logra que al acceder al directorio "backup.zip", en lugar de listarlo se acceda al archivo "backup.sh", y en lugar de volcar el contenido de dicho archivo se lo ejecute y se retorne su salida (asumiendo que el script producirá una salida adecuada para el protocolo HTTP).

Reiniciar Apache para que se tomen estos cambios:

# service apache2 reload

Gracias a esta solución, al acceder al directorio "backup.zip" automáticamente ofrece la descarga de todos los .txt comprimidos en un único ZIP. El usuario ni siquiera se entera de que "backup.zip" es un directorio en lugar de un archivo, y que el servidor está ejecutando un script que genera el archivo de manera dinámica por detrás.

¿Es una mierda esta solución? Sí. ¿Resuelve el problema de manera eficaz y eficiente? También.

Con este artículo quise demostrar lo fácil que es lograr hacer correr scripts Bash en modo CGI desde Apache.

Cabe destacar que, a fin de que esta solución funcione, es necesario contar con el módulo de Apache mod_cgi instalado y habilitado. Sin embargo, todas las instalaciones de Apache cuentan con esta configuración por defecto. Es posible habilitar mod_cgi en Debian y derivados ejecutando:

# a2enmod cgi
# service apache2 reload

Otras directivas interesantes de mod_cgi (además de AddHandler) son ScriptHandler y SetHandler, ambas utilizadas para establecer a mod_cgi como handler de archivos a nivel directorio. Para mayor información, recurrir a la documentación oficial del módulo cgi.

Referencias


Tal vez pueda interesarte


Compartí este artículo