LGEI FOCUS
GetTag
Uno strumento per CGI bash
di
Leonardo Serni
Quando, nel corso degli umani eventi, si rende necessario scrivere il classico CGI "quick and dirty", e - come me - si è alle primissime armi in Perl, oppure si preferisce comunque uno script della BASH per qualsiasi motivo, la difficoltà principale sta nel recepire le variabili ("tags") inviate dalla pagina HTML "madre".


Quando, nel corso degli umani eventi, si rende necessario scrivere il classico CGI "quick and dirty", e - come me - si è alle primissime armi in Perl, oppure si preferisce comunque uno script della Bourne-Again SHell per qualsiasi motivo, la difficoltà principale sta nel recepire le variabili ("tags") inviate dalla pagina HTML "madre".

Una form come questa

<PRE><FORM ACTION="/cgi-bin/test-cgi" METHOD=POST>
<INPUT TYPE=HIDDEN NAME=TEST VALUE=XTMMUORT>
Nome   : <INPUT TYPE=TEXT NAME=NAME
            VALUE="Your First Name Here" SIZE=25 MAXLENGTH=25>
Cognome: <INPUT TYPE=TEXT NAME=NAME2
            VALUE="Your Name Here" SIZE=25 MAXLENGTH=25>
Indir. : <INPUT TYPE=TEXT NAME=ADDRESS
            VALUE="Your Address Here" SIZE=25 MAXLENGTH=25>
Profes.: <SELECT NAME=ACTIVITY>
<OPTION VALUE="0" SELECTED>N/A
<OPTION VALUE="1">Architetto
...
<OPTION VALUE="99">Zuzzurellone
</SELECT>
<INPUT TYPE=SUBMIT VALUE="Invia">

immettendo come nome "Carcarlo Luigi Funiculà, Via Möbius 17"
farà sì che al programma test-cgi arrivi questa stringa in standard input e/o nella variabile ambiente QUERY_STRING:

CONTENT_TYPE = application/x-www-form-urlencoded
CONTENT_LENGTH = 87
TEST=XTMMUORT&NAME1=Carcarlo+Luigi&NAME2=Funicul%E0&ADDRESS=Via+M%F6bius+17&ACTIVITY=99

Se non fosse per i caratteri speciali, sarebbe semplicissimo decodificare una sequenza www-form-urlencoded usando il programma cut per separare le tags ("cut -d'&' -fn" darebbe la n-esima coppia tag=valore, dopodiché "cut -d'=' -f1" e "cut -d'=' -f2" darebbero rispettivamente il nome e il valore), e magari il comando tr per trasformare i "+" in spazi e, perché no?, alcune sequenze speciali nei caratteri corrispondenti: %E0 = à, e così via (anche i simboli %, =, ~ e & sono sottoposti a urlencode-escaping per non alterare il formato della query string; e mentre delle vocali accentate si può fare a meno, di quei simboli no... o per lo meno comincia a diventare una cosa piuttosto pelosa).

D'altra parte, benché io non ne sia sicurissimo, lanciare tot volte cut, tr, e magari grep, comincia a diventare una cosa pesante per il server.

Se lanciassi un solo programma esterno, anche dovendolo lanciare una volta per ogni tag, si realizzerebbe un considerevole risparmio.

  • La cosa più simpatica di tutte sarebbe poter settare variabili ambientali (tipo CGI_TEXT, CGI_NAME1, eccetera), però la shell bash è giustamente sospettosa di simili tentativi.
  • Sarebbe anche possibile pre-processare tutti i comandi in un file temporaneo, nel formato per esempio "TAGNAME=TAGVALUE", però una tag può occupare anche più di una riga.
  • Oppure ci si rassegna: il comando va ri-lanciato per ogni tag, per esempio "TAG=`echo "$INPUT" | gettag TAGNAME`".
Le virgolette attorno a $INPUT, che viene assegnata con "INPUT=`tee`" se si vuole leggere lo standard input, oppure riassegnando $QUERY_STRING (o magari entrambe le cose), non sono necessarie. Ma in genere, io non so cosa mi arriverà in quel CGI, e in alcuni casi è possibile che una stringa possa dare problemi se sfuggisse all'elaborazione e venisse interpretata in tutto o in parte come comando shell... per esempio inviare il file /etc/passwd via posta elettronica. Dato che non costa niente, "io" "metto" "virgolette" "ovunque" :-).

Ora (fermo restando che se uno scrivesse un CGI in Perl, farebbe molto meglio ad usare gli strumenti del Perl per estrarre i tags; è anche più sicuro da un punto di vista di cracking, perché violare una shell Bash è molto più facile che uscire da un modulo Perl CGI) vediamo come costruire un parser in linguaggio C.

Il sistema scelto è abbastanza brutale e riflette i miei tentativi iniziali di estrarre tutte le variabili e metterle a disposizione in forma "sicura" e "rapida" alla shell. Non ci sono riuscito (ehi, se qualcuno sa come fare, me lo dica), però gettag continua a leggersi tutte le tags dell'input anche dopo che ha trovato quella che gli è stata richiesta.

Inoltre, molte soluzioni qui presenti sono il classico esempio di come si usa un cannone per sparare alle zanzare.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#include <time.h>

/* Quando alloco memoria, ne alloco più pezzi contemporaneamente. Questo "rende"
   se l'operazione di malloc() è costosa o dove ha un grosso overhead in termini
   di memoria.
*/
#define GRANULARITY             16
#define MAX_BUFFER              0x1000
#define MAX_QUERY               0x4000

/*
   Non è consentito inviare tags di lunghezza arbitraria. Questo rende difficile
   esaurire la memoria del server inviando grosse quantità di dati.
*/
#define MAX_VARSIZE             40

/*
   Solo alcuni caratteri sono accettati. Li si può selezionare in compilazione.
   Questo set rende difficile inviare comandi, filesets ed espansioni mascherati
   (che richiedono uno o più di { \ $ | * / } ; ).
*/
#define ALLOWED_NAME_CHARS      "ABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789"
/* Questa #define dipende molto dal vostro compilatore. Al GCC piace. */
#define ALLOWED_VALUE_CHARS     "ABCDEFGHIJKLMNOPQRSTUVWXYZ_
 abcdefghijklmnopqrstuvwxyz
0123456789
+-%&pound;^~#.,:
àèéìòù
ç&sect;&deg;
<@>!$&{[(*\\|/)]}" /* Questi sono caratteri shell */

/*
   Le variabili sono memorizzate in due arrays: uno con i nomi, l'altro con i
   contenuti. Questo consente un certo risparmio rispetto all'uso di structs .
*/

static  char                    **entry = NULL;
static  char                    **value = NULL;
static  int                     entries = 0, maxent = 0;

/* Questa, è l'equivalente di un abort(), però consente (in teoria) di
   inviare un file dato il codice... in effetti io ho una directory di
   messaggi e la "mia" bailout(), normalmente, esegue l'equivalente di
       "cat $MESSAGEDIR/$rescode.html"
   prima di eseguire l'exit().
*/
int bailout(char *rescode)
{
        printf("%s", rescode);
        exit (-1);
}

/* Questa funzione, dato un puntatore al nome di un tag, ritorna il puntatore
   al suo valore -- oppure NULL, se il puntatore non esiste.
*/
char *tag(char *name)
{
        int i;
        for (i = 0; i < entries; i++)
                if (!strcasecmp(entry[i], name))
                        return value[i];
        return NULL;
}

/* Questa funzione, dato un puntatore al nome di un tag, ritorna il numero
   d'ordine della variabile che lo contiene, o -1 se non esiste quel tag.
   NOTA. Per come è usata qui la funzione tagnum, bastava (e costava uguale)
   la funzione tag(). Ma in altri contesti può venire buona tagnum: per es.
   se si vuole accedere alla struttura entry[x] direttamente occorre sapere
   il valore di x. La funzione add_entry più sotto è stata congegnata per
   usare tagnum anche se potrebbe farne a meno.
*/
int tagnum(char *name)
{
        int i;
        for (i=0; i < entries; i++)
                if (!strcasecmp(entry[i], name))
                        return i;
        return -1;
}

/*
    Aggiunge una coppia (nome, valore) all'array
*/

int add_entry(char *ent, char *val)
{
        int tn;
        char *update;
        /* Ovviamente ent e val non devono essere NULL */
        if (ent == NULL || val == NULL)
                bailout("err_tag");
        if ((tn=tagnum(ent))!=-1)
        {
                int newlen;
                /* Se la variabile già esisteva, viene modificata usando realloc e
                   il nuovo valore viene aggiunto in coda. Questo serve perché alcuni
                   tipi di tags, come CHECKBOX, sono cumulativi:
                   <INPUT TYPE=CHECKBOX NAME=CONTORNO VALUE=CR>Carote
                   <INPUT TYPE=CHECKBOX NAME=CONTORNO VALUE=CA>Carciofoletti
                   <INPUT TYPE=CHECKBOX NAME=CONTORNO VALUE=PO>Poponi
                   invierebbe, se si selezionasse un contorno di carote e poponi,
                        ...&CONTORNO=CR&CONTORNO=PO&...
                   Questo lo traduciamo noi con $CONTORNO uguale a "CR;PO". Certo,
                   il giorno che nel VALUE compare un punto e virgola, siamo nei guai.

                                      if strchr(val,';')
                        bailout ("err_tag");
                */
                newlen = strlen(value[tn])+strlen(val)+2;
                if ((value[tn]=realloc(value[tn],newlen))==NULL)
                        bailout("err_mem");
                strcat(value[tn],";");
                strcat(value[tn],val);
                return EXIT_SUCCESS;
        }
        if (entries == maxent)
        {
                /* Un certo numero di "entries" sono allocate tutte insieme. */
                maxent += GRANULARITY;
                if ((entry = (char **)realloc(entry, maxent * sizeof(char *))) == NULL)
                        bailout("err_mem");
                if ((value = (char **)realloc(value, maxent * sizeof(char *))) == NULL)
                        bailout("err_mem");
        }
        if ((entry[entries] = strdup(ent)) == NULL)
                bailout("err_mem");
        if ((value[entries] = strdup(val)) == NULL)
                bailout("err_mem");
        entries++;
        return EXIT_SUCCESS;
}

/*
    Questa funzione esegue il decoding vero e proprio, estraendo le coppie una alla
    volta.
    NOTA. Una delle variabili si chiama stat, e questo non è bene, se si vuole anche
    usare la funzione stat() ... cosa che qui non viene fatta, naturalmente.
    Eventualmente modificare in "status".
*/
int decode(char *source)
{
        /* stat == 0 significa che stiamo leggendo il nome della variabile,
           stat != 0 significa che ne stiamo leggendo il valore.
        */
        int     c, stat=0;
        char    buffer[2][MAX_BUFFER];
        int     buf_ptr = 0;
        char    *query, *base;

                /* Tutte le elaborazioni non sono fatte sulla stringa passata in ingresso,
            ma su una sua copia. Sempre che ci sia una stringa in ingresso; se però
            non c'è, ci limitiamo ad assumere che si debba leggere standard input.
        */
        if (source)
                base = query = strdup(source);
        else
        {
                size_t qsize;
                base = query = malloc(MAX_QUERY);
                if (base == NULL)
                        bailout("err_mem");
                qsize = fread(query, 1, MAX_QUERY - 1, stdin);
                query[qsize]=0;
        }
        /* A questo punto, query contiene o la stringa in ingresso oppure dati del
           tutto equivalenti prelevati da standard input. A seconda di come il CGI
           è organizzato, alcuni dati possono essere in QUERY_STRING, altri invece
           su standard input.
        */
        while(*query)
        {
                c = *(query++);
                /* Se i dati sono letti da STDIN, l'ultimo carattere è un EOF
                   e lo usiamo per forzare un flush */
                if (c == EOF)
                       c = '&';
                if (c == '&' || c == '=')
                {
                        buffer[stat][buf_ptr]=0;
                        if (stat)
                                add_entry(buffer[0], buffer[1]);
                        stat != stat;    /* Sia & che = implicano una commutazione
                                            tra lettura del nome e del valore
                                         */
                        buf_ptr = 0;
                        continue;
                }
                if (c == '+')            /* Caso speciale: lo spazio */
                        c = 0x20;
                else
                if (c == '%')            /* Caso speciale: l' escape */
                {
                        int hex;         /* Seguito da due cifre esadecimali */
                        c = 0;
                        /* Questo è un qualcosa che ricorda l'algoritmo di Horner.
                           I blocchi da "if" a "c+=hex" sono identici.
                           Da alcune prove, il GCC adora questa cosa, perche' il calcolo
                           impiega circa il 30% di tempo in meno. Prima, calcolavo
                           separatamente "hi" e "lo".
                        */
                        if (*query)
                                hex = *(query++);
                        else
                                hex = '4';
                        if (hex > '9') hex -= 'A'-0xA; else hex -= '0';
                        c += hex;
                        c <<= 4;
                        if (*query)
                                hex = *(query++);
                        else
                                hex = '4';
                        if (hex > '9') hex -= 'A'-0xA; else hex -= '0';
                        c += hex;
                }
                if (stat)
                {
                        if (strchr(ALLOWED_VAR_CHARS, c) != NULL)
                                buffer[0][buf_ptr++] = c;
                }
                else
                {
                        /* I nomi delle tags sono forzati maiuscoli */
                        c = toupper(c);
                        if (strchr(ALLOWED_NAME_CHARS, c) != NULL)
                                buffer[0][buf_ptr++] = c;
                }
                if (buf_ptr == MAX_BUFFER)
                        bailout("overflow");
        }
        /* Se c'è qualcosa (stat=1, e il buffer non è vuoto, cioè un nome è già in
           saccoccia e il valore relativo è non nullo), si aggiunge */
        if (stat && buf_ptr)
        {
                buffer[stat][buf_ptr] = 0;
                add_entry(buffer[0], buffer[1]);
        }
        free(base);
        return EXIT_SUCCESS;
}

int main(int argc, char **argv)
{
        if (argc!=2)
        {
                fprintf(stderr,"Syntax: tagset TAG_NAME\n");
                return EXIT_FAILURE;
        }
        /* Decodifichiamo prima QUERY_STRING, poi stdin */
        decode(getenv("QUERY_STRING"));
        decode(NULL);
        if (tag(argv[1]))
                printf("%s", tag(argv[1]));
        return EXIT_SUCCESS;
}

Questo consente di scrivere una pagina così fatta:


<HTML><BODY><FORM METHOD=POST ACTION="/cgi-bin/orderform"><PRE>
Nome    : <INPUT TYPE=TEXT NAME=NOME SIZE=25>
Cognome : <INPUT TYPE=TEXT NAME=COGNOME SIZE=25>
E-Mail  : <INPUT TYPE=TEXT NAME=EMAIL SIZE=25>

Volete abbonarvi a: <SELECT NAME=RIVISTA><OPTION VALUE="Vernacoliere">Vernacoliere
<OPTION VALUE="La Topa">La Topa (semestrale)
<OPTION VALUE="Papere">Le Papere del Papanti</SELECT>
I preziosi regalii da me scelti sono:
<INPUT TYPE=CHECKBOX NAME=REGALO VALUE="parago">Il pettine del catartico Mago Afono
<INPUT TYPE=CHECKBOX NAME=REGALO VALUE="popone">Scultura astratta vegetal-cubista
<INPUT TYPE=CHECKBOX NAME=REGALO VALUE="frenata">Mystico telo in cotone decorato
<HR>
<INPUT TYPE=CHECKBOX NAME=PRIVACY ASTUTELY CHECKED>Dò il consenso a che i miei dati siano
  rivenduti a prezzi esagerati a malfattori d'ogni genere
  che mi infastidiranno a domicilio
<TEXTAREA NAME=COMMENTI WIDTH=30 ROWS=5></TEXTAREA>
<INPUT TYPE=SUBMIT VALUE="Invia dati">
</PRE></FORM></BODY></HTML>



Questa form arriva ad un CGI bash, orderform, che a questo punto è banale scrivere:


#!/bin/sh

INPUT=`tee`

ADDRESS="root@localhost"

tag() { echo "$INPUT" | gettag $1; }

NOME=`tag NOME`
COGNOME=`tag COGNOME`
EMAIL=`tag EMAIL`
RIVISTA=`tag RIVISTA`
REGALO=`tag REGALO`
COMMENTI=`tag COMMENTI`
PRIVACY=`tag PRIVACY`

cat <<-MAILFORM | mail "$ADDRESS" -s "Order Form" 2>/dev/null
        Data: $( date )
        User: $REMOTE_HOST
        Brws: $HTTP_USER_AGENT
        Ref.: $HTTP_REFERER
        ----------------------------------
        Nome: $NOME
        Cogn: $COGNOME
        Mail: $EMAIL

        Abbonamento: $RIVISTA
        Regalo/i   : $REGALO

        Dà il consenso: $PRIVACY

        Commenti:
        $COMMENTI
        ----------------------------------
MAILFORM

cat <<-REPLY
        Content-type: text/html

        <HTML>
        <BODY>
                <H1>Ordine Inviato</H1>
                Congratvlazioni, signor $NOME $COGNOME!
                <BR>
                Da adesso lei &egrave; nostro Gradyto Cliente, ed ha diritto a
                tvtti i Privilegii & Disagii che qvesto comporta.
        </BODY>
        </HTML>
REPLY

exit 0;



Il risultato è processato due volte, in due sezioni here document. La prima si preoccupa di inviare i dati in mail, la seconda di rispondere all'utente in maniera appropriata.

Non sono fatti controlli sul nome o cognome ("if (test -z "$NOME"); then ..."), né viene verificata l'email (d'altra parte quasi nessun server supporta i comandi VRFY o EXPN. Rimane la possibilità di verificare che esista il server; magari alla prossima puntata...)

Comunque, al nostro utente, in questo caso root, arriva una mail ben ordinata così concepita:

Message 1:
From wwwrun@www.papanti.it  Wed Jun 17 13:30:30 1998
Date: Wed, 17 Jun 1998 13:30:30 +0200
From: Daemon user for apache <wwwrun@www.papanti.it>
To: root@papanti.it
Subject: Order Form

Data: Wed Jun 17 13:30:30 MEST 1998
User: 192.168.0.2
Brws: Mozilla/4.05 [en] (Win95; I)
Ref.: http://www.papanti.it/mail.html
----------------------------------
Nome: argilio
Cogn: papanti
Mail: papanti@papere.it

Abbonamento: Vernacoliere
Regalo/i   : frenata

Dà il consenso: on

Commenti:

----------------------------------


In questo modo diventa estremamente semplice costruirsi dei CGI su misura e realizzare addirittura interi database di clienti interamente residenti sulla macchina remota, che ci comunichino solo i dati essenziali estratti a colpi di grep da archivi in formato "solo testo". L'opzione è particolarmente interessante perché ci sono molti providers americani, Linux-friendly, che offrono web hosting con una quantità enorme di spazio disco e possibilità di scrivere i proprio CGI, purché in Perl o in bash; previo accordo con il webmaster, è possibile mettere su un sito very professional :-), con una velocità di connessione comparabile a quella di un provider italiano - o anche superiore - con dei costi irrisori; un sito particolarmente performante e di qualità veniva a costare sulle 800-900 mila annue allo sviluppatore (l'ultima volta che ne ho richiesto uno; your mileage may vary), compresi due anni di registrazione InterNIC, backups e quant'altro, e praticamente non richiede alcuna manutenzione.

Non so quali siano le tariffe dei providers italiani (e se lo sapessi, visto che la pubblicità comparativa in Italia è vietata, forse farei meglio a non riportarle), ma dubito che offrano altrettanta flessibilità; è più plausibile che per quel prezzo diano un po' di spazio per pagine statiche, che per i database ci sia un sovrapprezzo, e per l'uso di CGI custom un ulteriore e probabilmente leonino sovrapprezzo - o magari la richiesta di usare VisualBasic for Applications o qualche altro strumento inusitato.
 


Per l'articolo originale: © 1998 Leonardo Serni
Pubblicato sul n.6-Giugno 1998 di LGEI
Per l'edizione italiana: © 1998 LGEI