En los títulos y los textos vais a encontrar unas cuantas citaciones cinematográficas (y si, soy un cinéfilo). Si no os interesan podéis fingir no verlas, ya que no son fundamentales para la comprensión de los post...

Este blog es la versión en Español de mi blog en Italiano L'arte della programmazione in C. Espero que mis traducciones sean comprensibles...

sábado, 19 de mayo de 2018

El gran lighttpd
cómo escribir un módulo lighttpd en C - pt.4

Maude: ¿Y en que te dedicas en tu tiempo libre?
El Nota: Bueno, ya sabes: jugar a los bolos, conducir por ahí­, un viaje ácido de vez en cuando...
Dando por echo que para escribir un buen Software no hay necesidad de imitar los hábitos del mítico El Nota de El gran Lebowski, volvemos al tema lighttpd, porque en el tercer post de la serie (aquí, aquí y también aquí) prometí expandir la discusión. ¡Bien, ha llegado el momento, las promesas se mantienen!
...Bueno, ya sabes: jugar a los bolos, conducir por ahí­, un módulo lighttpd de vez en cuando...
Pues bien, en "El gran lighttpd - pt.3" (que acabáis de releer, supongo...) escribimos un agradable módulo elemental para lighttpd (era un clásico "Helloworld": escribía solo una presentación en el browser), pero era un buen comienzo para entrar en el mundo de los módulos para Web Servers. Vale, ahora vamos a reanudar ese módulo y les vamos a añadir una función que extrae los datos POST incluidos en una eventual petición HTTP de tipo POST que llega al módulo. ¿Por qué he elegido añadir esa funcionalidad? Bueno, obviamente porque es bastante normal que un módulo cuente con varios tipos de peticiones (GET, POST, etc.), y luego, en particular, recuerdo que la primera vez que añadí esta funcionalidad en un módulo Apache que escribí, me di cuenta de que no estaban disponibles muchas explicaciones sobre este tema (y esto a pesar de la enorme cantidad de documentación de Apache disponible respeto a la de lighttpd)... os dejo imaginar, por lo tanto, cuánta documentación y cuántos ejemplos encontré para hacer lo mismo con lighttpd: prácticamente nada.

Pues bien, sin molestarse en repetir todo el código y la forma de generarlo, sólo reescribiremos la función mod_helloworld_uri_handler() y añadiremos una nueva función getPost(). Añado que, para integrar el código que he escrito, es necesario seguir la guía de instalación indicada en "El gran lighttpd - pt.1" y repetir los 10 pasos indicados en "El gran lighttpd - pt.2", usando esta vez la versión 1.4.45 de lighttpd (ojo: usando una versión anterior, el código que estoy a punto de mostrar no funciona). Hecho? entonces ¡Vamos con el código
URIHANDLER_FUNC(
    mod_helloworld_uri_handler)
{
    plugin_data *p = p_d;

    UNUSED(srv);

    // test modo (return si error)
    if (con->mode != DIRECT)
        return HANDLER_GO_ON;

    // test uri path (return si error)
    size_t s_len = buffer_string_length(con->uri.path);
    if (s_len == 0)
        return HANDLER_GO_ON;

    mod_helloworld_patch_connection(srv, con, p);

    // test handler (return si error)
    if (con->uri.path->ptr && strstr(con->uri.path->ptr, "helloworld")) {
        // prepara el buffer para la respuesta
        buffer *buf = buffer_init();

        // test método http
        if (strstr(con->request.request->ptr, "GET")) {
            // método GET: escribe respuesta
            buffer_append_string(buf, "<big>Hello, world!</big>");
        }
        else if (strstr(con->request.request->ptr, "POST")) {
            // método POST: controla la presencia de post-data
            size_t len = con->request.content_length;
            if (len) {
                // post-data presentes; los lee para escribir la respuesta
                char *data = malloc(len + 1);
                if (readPost(con, data, len)) {
                    // escribe la respuesta
                    buffer_append_string(buf, data);
                }

                // libera la memoria
                free(data);
            }
            else {
                // error: mensaje POST sin post-data
                buffer_append_string(buf, "mod_helloworld: error: POST sin post-data");
            }
        }
        else {
            // error: mensaje con método no tratado
            buffer_append_string(buf, "mod_helloworld error: método no tratado");
        }

        // escribe el buffer y lo libera
        chunkqueue_append_buffer(con->write_queue, buf);
        buffer_free(buf);

        // envía el header
        response_header_overwrite(
                srv, con, CONST_STR_LEN("Content-Type"), CONST_STR_LEN("text/html"));
        con->http_status = 200;
        con->file_finished = 1;

        // handling terminado
        return HANDLER_FINISHED;
    }

    // handler no encontrado
    return HANDLER_GO_ON;
}

static bool readPost(
    connection *con,                // datos conexión
    char       *data,               // buffer destinación para post-data
    size_t     len)                 // longitud data (sin terminador)
{
    // set valor de return de default
    int retval = false;

    // lee post-data (en el stream de chunks)
    size_t rpos = 0;
    chunkqueue *cq = con->read_queue;
    for (chunk *mychunk = cq->first; mychunk; mychunk = cq->first) {
        // calcula el size del buffer correspondiente al chunk de post-data corriente
        size_t n_tocopy = buffer_string_length(mychunk->mem) - mychunk->offset;

        // test si hay datos a copiar
        if (n_tocopy <= (len - rpos)) {
            // copia un chunk y set de la posición de copia del proximo chunk
            memcpy(data + rpos, mychunk->mem->ptr + mychunk->offset, n_tocopy);
            rpos += n_tocopy;

            // ha sido leído (almeno) un chunk de post-data: set retval=true
            retval = true;
        }
        else {
            // buffer overflow: salida forzada del loop
            break;
        }

        // indica como leído el chunk de post-data corriente
        chunkqueue_mark_written(cq, chunkqueue_length(cq));
    }

    // añade el terminadore de string (el buffer es largo len+1)
    data[len] = 0;

    // sale con retval
    return retval;
}
Ok, como se nota es ampliamente comentado y así se auto-explica, por lo cual no voy a detenerme sobre las instrucciónes y/o grupos de instrucciones (¡leer los comentarios! ¡Están ahí para eso!), pero voy a añadir, solamente, algunos detalles estructurales.

la función original mod_helloworld_uri_handler() sigue siendo una especie de función "Helloworld", que ahora distingue los tipos de petición y, en el caso POST, llama a la función readPost() y escribe  como respuesta "Hello, world!" mas los datos. En el caso de peticiones GET se limita a escribir "Hello, world!" y, para otros tipos de peticiones o con peticiones POST sin datos, sale con error.

La función readPost() es simple, pero no es inmediata: dada la casi total falta de ejemplos disponibles en la red, he tenido que hacer, prácticamente,   reverse engineering sobre los (excelentes) módulos integrados en la distribución (¡tener el código fuente original disponible siempre es una gran cosa!) de manera de deducir cómo hacer lo que tenia como objetivo. El resultado final (modestia aparte) es bueno, tanto en apariencia (estilo) como en rendimiento (hice algunas pruebas y funciona bien).

¡Ah, el test, se me olvidaba! Testear con peticiones POST no es simple cómo hacerlo con las GET, adonde es suficiente (como dicho en el post anterior) escribir la URI del modulo en la barra de direcciones de Firefox o Chrome (o cualquier otro navegador menos IE, por favor...) y esperar la respuesta. Para probar el nuestro nuevo módulo es mejor usar un buen plugin (como Poster, por ejemplo) que nos facilitará mucho la tarea.

Vale, de momento, creo que podemos parar por un tiempo el tema módulos de lighttpd. Juro que en el próximo post voy a hablar de otra cosa, no me gustaría que piensen que el C se utiliza sólo para Web Servers...

¡Hasta el próximo post!