Nei titoli e nei testi troverete qualche rimando cinematografico (ebbene si, sono un cinefilo). Se non vi interessano fate finta di non vederli, già che non sono fondamentali per la comprensione dei post...

Di questo blog ho mandato avanti, fino a Settembre 2018, anche una versione in Spagnolo. Potete trovarla su El arte de la programación en C. Buona lettura.

venerdì 18 maggio 2018

Il grande lighttpd
come scrivere un modulo lighttpd in C - pt.4

Maude: E di cosa ti occupi nel tempo libero?
Drugo: Mah, le solite cose: Bowling, un giro in macchina, un trip d'acido quando capita...
Sottinteso che per scrivere del buon Software non c'è bisogno di imitare le abitudini del mitico Drugo del Grande Lebowski, ritorniamo sull'argomento lighttpd, perchè nel terzo post della serie (qui, qui e ancora qui) avevo promesso di ampliare il discorso. Ebbene è venuto il momento, le promesse si mantengono!
...Mah, le solite cose: Bowling, un giro in macchina, un modulo lighttpd quando capita...
Bene, ne "Il grande lighttpd - pt.3" (che, avete appena riletto, immagino...) avevamo scritto un bel modulo elementare per lighttpd. Faceva poco (era un classico "Helloworld": scriveva solo una presentazione nel browser) però era un buon inizio per entrare nel mondo dei moduli di questo notevole Web Server. Ecco, adesso riprenderemo la base fatta a suo tempo e gli aggiungeremo una funzione che estrae i dati POST inclusi in una eventuale petizione HTTP di tipo POST che arriva al modulo. Perché ho scelto di aggiungere proprio questa funzionalità? Beh, ovviamente perché è abbastanza normale che un modulo tratti vari tipi di petizioni (GET, POST, ecc.), e poi, in particolare, ricordo che la prima volta che aggiunsi questa funzionalità in un modulo Apache che avevo scritto, notai che non erano disponibili molte indicazioni su questo argomento (e questo nonostante la enorme mole di documentazione su Apache disponibile rispetto a quella su lighttpd)... vi lascio immaginare, quindi, quanta documentazione e quanti esempi ho trovato per fare la stessa cosa per lighttpd: praticamente niente.

Bene, senza stare a ripetere tutto il codice e la maniera di generarlo, riscriveremo solo la funzione mod_helloworld_uri_handler() e aggiungeremo una nuova funzione getPost(). Premetto che, per integrare il codice che ho scritto, è necessario seguire la guida di installazione indicata in "Il grande lighttpd - pt.1" e ripetere tutti i 10 passi indicati in "Il grande lighttpd - pt.2", usando questa volta la versione 1.4.45 di lighttpd (occhio: se usate una versione più vecchia il codice che sto per mostrarvi non funziona). Fatto? Allora vai col codice!
URIHANDLER_FUNC(
    mod_helloworld_uri_handler)
{
    plugin_data *p = p_d;

    UNUSED(srv);

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

    // test uri path (return se errore)
    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 se errore)
    if (con->uri.path->ptr && strstr(con->uri.path->ptr, "helloworld")) {
        // prepara il buffer per la risposta
        buffer *buf = buffer_init();

        // test metodo http
        if (strstr(con->request.request->ptr, "GET")) {
            // metodo GET: scrive risposta
            buffer_append_string(buf, "<big>Hello, world!</big>");
        }
        else if (strstr(con->request.request->ptr, "POST")) {
            // metodo POST: controlla la presenza di post-data
            size_t len = con->request.content_length;
            if (len) {
                // post-data presenti; li legge per scrivere la risposta
                char *data = malloc(len + 1);
                if (readPost(con, data, len)) {
                    // scrive la risposta
                    buffer_append_string(buf, data);
                }

                // libera la memoria
                free(data);
            }
            else {
                // errore: messaggio POST senza post-data
                buffer_append_string(buf, "mod_helloworld: errore: POST senza post-data");
            }
        }
        else {
            // errore: messaggio con metodo non trattato
            buffer_append_string(buf, "mod_helloworld errore: metodo non trattato");
        }

        // scrive il buffer e lo libera
        chunkqueue_append_buffer(con->write_queue, buf);
        buffer_free(buf);

        // spedisce l'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 finito
        return HANDLER_FINISHED;
    }

    // handler non trovato
    return HANDLER_GO_ON;
}

static bool readPost(
    connection *con,                // dati connessione
    char       *data,               // buffer destinazione per post-data
    size_t     len)                 // lunghezza data (senza terminatore)
{
    // set valore di return di default
    int retval = false;

    // legge post-data (nello stream di chunks)
    size_t rpos = 0;
    chunkqueue *cq = con->read_queue;
    for (chunk *mychunk = cq->first; mychunk; mychunk = cq->first) {
        // calcola il size del buffer corrispondente al chunk di post-data corrente
        size_t n_tocopy = buffer_string_length(mychunk->mem) - mychunk->offset;

        // test se ci sono dati da copiare
        if (n_tocopy <= (len - rpos)) {
            // copia un chunk e set della posizione di copia del prossimo chunk
            memcpy(data + rpos, mychunk->mem->ptr + mychunk->offset, n_tocopy);
            rpos += n_tocopy;

            // é stato letto (almeno) un chunk di post-data: set retval=true
            retval = true;
        }
        else {
            // buffer overflow: forzo uscita dal loop
            break;
        }

        // segnalo come letto il chunk di post-data corrente
        chunkqueue_mark_written(cq, chunkqueue_length(cq));
    }

    // aggiungo il terminatore di stringa (il buffer é lungo len+1)
    data[len] = 0;

    // esco con retval
    return retval;
}
Ok, come vedete è ampiamente commentato e quindi è auto-esplicativo, per cui non mi dilungherò sulle singole istruzioni e/o gruppi di istruzioni (leggete i commenti! sono li per quello!), ma aggiungerò, solo, qualche dettaglio strutturale.

L'originale funzione mod_helloworld_uri_handler() è ancora una sorta di funzione "Helloworld", ma ora distingue i tipi di petizione e, nel caso POST, chiama la funzione readPost() e scrive come risposta "Hello, world!" più i dati POST. Nel caso di petizioni GET si limita a scrivere "Hello, world!" e, con altri tipi di petizione o con petizioni POST senza dati, scrive un messaggio di un errore.

La funzione readPost() è semplice ma non immediata: vista la scarsezza quasi totale di esempi disponibili in rete ho dovuto, praticamente, fare reverse engineering sugli (ottimi) moduli integrati nella distribuzione (avere il codice sorgente originale a disposizione è sempre una cosa ottima!) in maniera di dedurre come fare quello che mi ero prefisso. Il risultato finale è (modestia a parte) buono, sia come aspetto (stile) sia come prestazioni (ho fatto dei test e funziona bene).

Ah, il test, dimenticavo! Testare con petizioni POST non è semplice come farlo con le GET, dove è sufficiente (come detto nel post precedente) scrivere la URI del modulo nella barra degli indirizzi di Firefox o Chrome (o qualsiasi altro browser meno IE, per favore...) e aspettare la risposta. Per testare il nostro nuovo modulo è meglio usare un bel plugin del browser (tipo Poster) per semplificare il lavoro.

Va bene, per il momento credo che possiamo fermare per un tempo l'argomento moduli lighttpd. Giuro che nel prossimo post parlerò d'altro, non vorrei che pensaste che il C si usa solo per i Web Servers...

Ciao e al prossimo post!

Nessun commento:

Posta un commento