CGI e applicazioni web dinamiche
In questa pagina 10
Un server HTTP che serve file (HTTP 0.9 e 1.0 - struttura dei messaggi, header e parsingHTTP/0.9 (1991) ha una sola riga di richiesta "GET /percorso" e la risposta e' solo il documento HTML, senza codici di stato ne' header, e la connessione si chiude alla fine; HTTP/1.0 (RFC 1945) aggiunge Request-Line con versione, Status-Line con codice, header (generali, di richiesta, di risposta, di entita'), i metodi HEAD e POST, tipi di contenuto diversi dall'HTML; un messaggio e' riga iniziale, header "Nome: valore" a righe CRLF, riga vuota, corpo; in HTTP/1.0 il corpo della risposta finisce quando il server chiude la connessione, mentre per POST serve Content-Length; il parsing legge la prima riga, poi gli header fino alla riga vuota, poi il corpo; i nomi degli header non distinguono maiuscole e minuscole, e un client robusto accetta LF semplice e spazi variabili.HTTP 0.9 e 1.0 - struttura dei messaggi, header e parsing →) produce solo pagine statiche. Per generare contenuti calcolati al momento (il risultato di una ricerca, la pagina di un utente, una somma) serve un programma. La Common Gateway Interface (CGI) è la prima soluzione standardizzata: un'interfaccia fra il server web e un programma esterno che genera la risposta. È definita in RFC 3875 (CGI Version 1.1, 2004), documento informativo che descrive la prassi nata nel 1993 con il server NCSA.
Idea e architettura
Il server non sa generare il contenuto; sa lanciare un programma e usarne l'uscita come risposta. Per ogni richiesta destinata a un programma CGI (di solito un URL sotto /cgi-bin/) il server:
- costruisce l'ambiente del programma: le meta-variabili con i dati della richiesta;
- crea un processo figlio (
fork) e lancia il programma (exec), con standard input collegato al corpo della richiesta e standard output collegato al server (due pipe); - legge tutto ciò che il programma scrive, ne interpreta gli header CGI e lo trasforma in una risposta HTTP;
- attende la fine del processo (
waitpid).
client server web programma CGI
| GET /cgi-bin/somma?a=3&b=4 | |
|----------------------------->| fork + exec |
| | ambiente: QUERY_STRING=a=3&b=4,
| | REQUEST_METHOD=GET, ...
| |------------------------->| calcola
| | stdout: Content-Type: text/plain
| | (riga vuota)
| | 3 + 4 = 7
| |<-------------------------| termina
| HTTP/1.1 200 OK ... | aggiunge Date, Content-Length, ...
|<-----------------------------|Il programma può essere scritto in qualunque linguaggio: deve solo leggere variabili d'ambiente e standard input e scrivere su standard output (shell, C, Perl, Python...). Non deve sapere nulla di socket né di HTTP oltre ai pochi header della risposta: questo è il vantaggio di CGI. Lo svantaggio è che un processo nuovo per ogni richiesta è costoso (sotto).
La richiesta: meta-variabili e standard input
Il server passa i dati della richiesta con variabili d'ambiente (RFC 3875 par. 4.1):
| variabile | contenuto | esempio |
|---|---|---|
REQUEST_METHOD |
metodo HTTP, maiuscolo | GET, POST |
QUERY_STRING |
parte dopo ? dell'URL, non decodificata (vuota se assente) |
a=3&b=4 |
CONTENT_LENGTH |
byte del corpo (vuota se non c'è) | 7 |
CONTENT_TYPE |
tipo MIME del corpo | application/x-www-form-urlencoded |
SCRIPT_NAME |
percorso che identifica il programma | /cgi-bin/somma |
PATH_INFO |
resto del percorso dopo SCRIPT_NAME, decodificato |
/extra/path |
SERVER_NAME, SERVER_PORT |
nome (senza porta) e porta del server | www.example.com, 80 |
SERVER_PROTOCOL |
versione della richiesta | HTTP/1.1 |
SERVER_SOFTWARE |
nome del server | mini-cgi/1.0 |
GATEWAY_INTERFACE |
versione CGI | CGI/1.1 |
REMOTE_ADDR |
indirizzo IP del client | 192.0.2.7 |
HTTP_* |
ogni header della richiesta, con il nome in maiuscolo e - sostituito da _ e prefisso HTTP_ |
HTTP_USER_AGENT, HTTP_COOKIE |
AUTH_TYPE, REMOTE_USER |
autenticazione eseguita dal server | Basic, mario |
Esempio. Con la richiesta GET /cgi-bin/somma/extra?a=3&b=4 il programma somma trova SCRIPT_NAME=/cgi-bin/somma, PATH_INFO=/extra, QUERY_STRING=a=3&b=4: la parte di URL che identifica il programma e quella che il programma interpreta restano separate.
Content-Length e Content-Type non diventano HTTP_CONTENT_LENGTH: hanno le variabili proprie, senza prefisso. L'header Authorization di norma non viene passato (il server fornisce AUTH_TYPE e REMOTE_USER).
Il corpo della richiesta (POST) arriva sullo standard input del programma. Il programma legge esattamente CONTENT_LENGTH byte: la RFC specifica che non deve leggere di più perché il server non è obbligato a segnalare la fine del file. Un programma che legge fino a EOF può restare bloccato.
GET : i dati stanno in QUERY_STRING (stdin vuoto)
POST: i dati stanno su stdin (CONTENT_LENGTH byte, tipo in CONTENT_TYPE)I dati di un modulo sono nome=valore separati da &, in percent-encoding con + per lo spazio (Caching, autenticazione, tipi MIME e URI in HTTPLa cache HTTP riusa le risposte senza contattare il server finche' sono fresche (Cache-Control: max-age, Expires; in mancanza una durata euristica pari a circa il 10% del tempo trascorso da Last-Modified) e, quando sono scadute, le rivalida con una richiesta condizionale (If-None-Match con ETag, If-Modified-Since con Last-Modified) alla quale il server risponde 304 senza corpo; no-store vieta di memorizzare, no-cache obbliga a rivalidare, private esclude le cache condivise. L'autenticazione usa 401 con WWW-Authenticate e la risposta Authorization: Basic (base64 di utente:password, solo sopra TLS) o Digest (hash con nonce); 407 vale per i proxy. Content-Type porta il tipo MIME (tipo/sottotipo; parametri come charset e boundary), Accept e gli altri header Accept-* guidano la negoziazione. Un URI (RFC 3986) e' scheme://userinfo@host:porta/percorso?query#frammento, con percent-encoding %HH per i byte non ammessi e regole per risolvere i riferimenti relativi.Caching, autenticazione, tipi MIME e URI in HTTP →): il programma deve dividere e decodificare.
La risposta: header CGI e corpo
Il programma scrive sullo standard output un messaggio che ha la forma di un messaggio HTTP senza riga di stato: header, riga vuota, corpo. (RFC 3875 par. 6.2)
Content-Type: text/plain; charset=utf-8
3 + 4 = 7| header CGI | significato |
|---|---|
Content-Type |
obbligatorio se c'è un corpo (tipo MIME) |
Status: 404 Not Found |
stato HTTP da usare; se manca vale 200 OK |
Location: <URI> |
reindirizzamento |
altri (Set-Cookie, Cache-Control...) |
inoltrati al client |
Quattro forme di risposta:
- document response:
Content-Type, eventualeStatus, altri header, riga vuota, corpo; - client redirect: solo
Locationcon un URI assoluto: il server genera un302 Found; - client redirect con documento:
LocationpiùStatus: 302...,Content-Typee un corpo; - local redirect:
Locationcon solo il percorso (/altra/pagina): il server rielabora internamente la richiesta come se il client avesse chiesto quel percorso, senza passare dal client. Il server semplice dell'esercizio tratta anche questo caso come un302.
Il server completa il messaggio: aggiunge la status line (HTTP/1.1 200 OK), Date, Server, Content-Length (lo conosce perché ha letto tutto l'output del programma, oppure usa il chunked se lo inoltra mentre arriva) e Connection. Per la richiesta sopra la risposta al client è:
HTTP/1.1 200 OK
Date: Sun, 11 Oct 2026 21:00:00 GMT
Server: mini-cgi/1.0
Content-Type: text/plain; charset=utf-8
Content-Length: 10
Connection: close
3 + 4 = 7(il corpo 3 + 4 = 7 con il \n finale ha 10 byte). I terminatori di riga nell'uscita del programma possono essere \r\n o semplicemente \n: il server accetta entrambi. Esiste anche una variante, gli script NPH (non-parsed header, par. 5, nome nph-...), il cui output è già un messaggio HTTP completo con status line, che il server inoltra senza modifiche.
Se il programma fallisce (codice di uscita diverso da zero, nessun output, header malformati) il server risponde 500 Internal Server Error o 502 Bad Gateway: il server è un gateway verso un altro sistema (DNS, proxy web, HTTP CONNECT e gateway applicativiUn programma risolve i nomi con getaddrinfo (file hosts e poi DNS); un messaggio DNS (RFC 1035) ha un header di 12 byte (ID, flag QR/RD/RA/TC e RCODE, quattro contatori), una domanda (QNAME a etichette con lunghezza, QTYPE, QCLASS) e record di risposta con nomi eventualmente compressi da puntatori, su UDP porta 53 con ripiego su TCP se il bit TC e' acceso; un web proxy e' un intermediario scelto dal client che riceve richieste con URI assoluto, le inoltra al server (togliendo gli header hop-by-hop, aggiungendo Via e X-Forwarded-For) e puo' memorizzare le risposte; le cache si organizzano in gerarchie e con funzioni hash coerenti, le CDN le replicano vicino agli utenti; il metodo CONNECT chiede al proxy un tunnel TCP verso host:porta e dopo la risposta 2xx il proxy inoltra i byte in entrambe le direzioni senza interpretarli (HTTPS, con restrizione delle porte); un gateway o reverse proxy e' un intermediario scelto dal server che traduce o inoltra le richieste ad altri sistemi (bilanciamento, terminazione TLS, FastCGI, API gateway).DNS, proxy web, HTTP CONNECT e gateway applicativi →).
Esempi di programmi CGI
Un programma in shell, che mostra le meta-variabili (chmod +x hello.sh e copia in cgi-bin/):
#!/bin/sh
# hello.sh - programma CGI in shell: mostra alcune meta-variabili (copiarlo in www/cgi-bin/ e chmod +x)
echo "Content-Type: text/plain; charset=utf-8"
echo ""
echo "Ciao dal CGI!"
echo "REQUEST_METHOD = $REQUEST_METHOD"
echo "QUERY_STRING = $QUERY_STRING"
echo "SCRIPT_NAME = $SCRIPT_NAME"
echo "PATH_INFO = $PATH_INFO"
echo "SERVER_PROTOCOL = $SERVER_PROTOCOL"
echo "REMOTE_ADDR = $REMOTE_ADDR"
echo "HTTP_USER_AGENT = $HTTP_USER_AGENT"Un programma in C, che somma due numeri passati con GET (query) o POST (stdin):
/* somma.c - programma CGI: somma i parametri a e b, passati con GET (QUERY_STRING) o POST (stdin).
* Compilare: gcc -Wall -Wextra -o somma somma.c e copiare l'eseguibile in www/cgi-bin/
* Provare: curl "http://127.0.0.1:8080/cgi-bin/somma?a=3&b=4" curl -d "a=3&b=4" http://127.0.0.1:8080/cgi-bin/somma
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* Cerca "nome=" in una stringa del tipo "a=3&b=4" e torna il valore intero (0 se manca). */
static long param(const char *qs, const char *name)
{
size_t k = strlen(name);
for (const char *p = qs; p != NULL && *p; ) {
if (strncmp(p, name, k) == 0 && p[k] == '=')
return strtol(p + k + 1, NULL, 10);
p = strchr(p, '&'); /* salta al parametro successivo */
if (p != NULL)
p++;
}
return 0;
}
int main(void)
{
const char *method = getenv("REQUEST_METHOD");
char data[1024] = "";
if (method != NULL && strcmp(method, "POST") == 0) {
/* POST: i dati stanno sullo stdin; CONTENT_LENGTH dice quanti byte leggere (non c'e' EOF garantito). */
const char *cl = getenv("CONTENT_LENGTH");
size_t n = cl ? (size_t)atol(cl) : 0;
if (n >= sizeof data)
n = sizeof data - 1;
size_t got = fread(data, 1, n, stdin);
data[got] = '\0';
} else {
const char *qs = getenv("QUERY_STRING"); /* GET: i dati stanno nell'ambiente */
snprintf(data, sizeof data, "%s", qs ? qs : "");
}
long a = param(data, "a"), b = param(data, "b");
/* Uscita CGI: header, RIGA VUOTA, corpo. Il server aggiunge Content-Length, Date ecc. */
printf("Content-Type: text/plain; charset=utf-8\r\n");
printf("\r\n");
printf("%ld + %ld = %ld\n", a, b, a + b);
return 0;
}Si compila con gcc -o somma somma.c e l'eseguibile si copia in www/cgi-bin/. Due passaggi da notare: per il POST si legge CONTENT_LENGTH byte con fread; la riga vuota (printf("\r\n")) dopo gli header è obbligatoria, senza la quale il server non riconosce l'uscita come valida.
Un programma che reindirizza il client:
#!/bin/sh
# vai.sh - programma CGI che reindirizza il client: con solo Location (URI assoluto) il server risponde 302 Found
# (RFC 3875 par. 6.2.3). L'URI assoluto si costruisce con le meta-variabili SERVER_NAME e SERVER_PORT.
echo "Location: http://$SERVER_NAME:$SERVER_PORT/cgi-bin/hello.sh?da=vai"
echo ""Il lato server: come si lancia un programma
Il server (completo in Esercizio - Server HTTP con CGI (sul modello della prova pratica)) fa, per ogni richiesta CGI, questi passaggi, usando le chiamate di System call POSIX, file descriptor e API delle socketLe system call sono le funzioni con cui un programma in user space chiede servizi al kernel (open, read, write, close, fork, pipe, dup2, socket, bind, listen, accept, connect); restituiscono -1 e impostano errno in caso di errore; un file descriptor e' un intero che indicizza la tabella dei file aperti del processo (0 stdin, 1 stdout, 2 stderr) e vale per file, pipe e socket; read e write possono trasferire MENO byte del richiesto (e sui socket TCP non c'e' nessun confine fra i messaggi), quindi servono cicli write_all e read_exact; l'API delle socket crea un punto finale di comunicazione (socket), lo lega a indirizzo e porta (bind), lo rende passivo (listen), accetta connessioni (accept) o si connette (connect); getaddrinfo traduce nomi e porte in indirizzi (IPv4 e IPv6); UDP usa sendto e recvfrom e conserva i confini dei datagrammi, TCP e' uno stream affidabile.System call POSIX, file descriptor e API delle socket →:
int inp[2], outp[2];
pipe(inp); pipe(outp); /* inp: server -> programma (stdin); outp: programma -> server (stdout) */
pid_t pid = fork();
if (pid == 0) { /* FIGLIO */
dup2(inp[0], STDIN_FILENO); /* stdin del programma = estremita' di lettura di inp */
dup2(outp[1], STDOUT_FILENO); /* stdout del programma = estremita' di scrittura di outp */
close(inp[0]); close(inp[1]); close(outp[0]); close(outp[1]); /* le copie originali non servono piu' */
alarm(10); /* timeout: SIGALRM uccide il programma se si blocca */
execle(script, name, (char *)NULL, envp); /* lancia il programma con l'ambiente costruito */
_exit(127); /* se exec fallisce */
}
close(inp[0]); close(outp[1]); /* PADRE: chiude le estremita' che non usa */
write(inp[1], body, clen); close(inp[1]); /* corpo della POST; la close fa vedere EOF al programma */
/* ... read(outp[0]) in ciclo fino a EOF, poi waitpid(pid) ... */Perché ogni dettaglio conta:
dup2prima diexec: i descrittori restano aperti attraversoexec; il programma "vede" stdin e stdout già collegati alle pipe.- Chiusura delle estremita' inutilizzate. Il padre deve chiudere
outp[1]: finché una copia dell'estremità di scrittura resta aperta nel padre,read(outp[0])non ritorna mai0(EOF) e il server resta bloccato. Allo stesso modoclose(inp[1])dopo il corpo serve al programma per vedere la fine dell'input. - Deadlock. Se il server scrive un corpo molto lungo mentre il programma scrive molto output senza leggere l'input, le due pipe (capienza tipica 64 KiB) si riempiono e i processi si bloccano a vicenda. Si evita limitando la dimensione del corpo (nell'esercizio meno di 64 KiB) oppure leggendo e scrivendo con
poll. waitpid: serve a raccogliere il figlio (niente zombie) e a leggerne il codice di uscita.- Timeout e limiti:
alarmprima diexec(il timer sopravvive aexec), un tetto alla dimensione dell'output, altrimenti un programma difettoso blocca o riempie la memoria del server. - Ambiente costruito dal server: non si passa l'ambiente del server così com'è (potrebbe contenere segreti); si costruisce un array
envpcon le sole variabili necessarie.
Il parsing dell'uscita (cgi_parse nell'esercizio) cerca la prima riga vuota, legge gli header Status, Content-Type, Location, e considera il resto il corpo.
Sicurezza
Un programma CGI esegue codice guidato da dati inviati da un estraneo. Le trappole classiche:
- Command injection. Costruire un comando di shell con
system()opopen()usando parametri della richiesta (system("grep " + parametro)) permette di eseguire comandi arbitrari con;,|,`. Si usanoexeccon argomenti separati e si convalidano i valori. - Path traversal. Il nome del programma e i percorsi che il programma apre non devono poter contenere
..o/: il server dell'esercizio accetta solo nomi fatti di lettere, cifre,_,-,.. - Buffer overflow. Copiare
QUERY_STRINGin un buffer fisso senza controllare la lunghezza. - Header injection. Mettere nell'uscita, in un header (
Location,Set-Cookie), valori dell'utente con\r\n: permette di iniettare header o un'altra risposta. - Variabili
HTTP_*: contengono dati controllati dal client. Il caso storico è Shellshock (2014): bash interpretava codice contenuto nel valore di una variabile d'ambiente (comeHTTP_USER_AGENT), e i CGI in shell diventavano eseguibili da remoto. Un secondo caso è httpoxy: l'headerProxydiventaHTTP_PROXY, che molte librerie HTTP usano come proxy di uscita; per questo il server non passa mai l'headerProxy. - Denial of service. Un processo per richiesta è facile da esaurire: servono limiti di tempo, di memoria e di numero di processi.
- Privilegi. I programmi girano con i diritti del server: meglio un utente senza privilegi, e solo i programmi nella cartella
cgi-binmarcati eseguibili.
Limiti e alternative
Creare un processo (e avviare un interprete) per ogni richiesta è lento: centinaia di microsecondi o millisecondi di avvio, nessuna memoria condivisa, nessuna connessione persistente a una base di dati. Le soluzioni successive tengono vivo il programma:
Resta valida l'idea di CGI: il server delega la generazione del contenuto a un programma e gli passa la richiesta in un formato definito; è anche il modello con cui un server HTTP si comporta da gateway applicativo.
Esercizi collegati
- Esercizio - Server HTTP con CGI (sul modello della prova pratica): server con
fork, pipe,dup2,execle, parsing dell'uscita. - Esercizio - Parsing di request line, header e URI (sul modello della prova pratica): percent-decoding e query string.
Errori tipici
- Dimenticare la riga vuota fra gli header CGI e il corpo, oppure
Content-Type. - Leggere lo standard input fino a EOF invece di
CONTENT_LENGTHbyte. - Non chiudere le estremità delle pipe nel padre (il server si blocca) o nel figlio (il programma non vede EOF).
- Dimenticare
waitpid(zombie) o il timeout (un programma bloccato blocca il server). - Usare
system()con parametri della richiesta (injection), copiareQUERY_STRINGsenza limiti (overflow). - Aspettarsi
QUERY_STRINGdecodificata: è il programma a decodificare (+,%XX);PATH_INFOinvece è già decodificata. - Dimenticare che
execsostituisce il processo: il codice dopoexecesegue solo seexecfallisce (serve_exit). - Credere che CGI sia veloce: un processo per richiesta.
Domande d'esame
1. Descrivere come un server web esegue un programma CGI per una POST con corpo di 7 byte: cosa riceve e cosa produce il programma?
Traccia. Il server crea due pipe e fa fork; nel figlio dup2 collega stdin e stdout alle pipe e exec lancia il programma con l'ambiente (REQUEST_METHOD=POST, CONTENT_LENGTH=7, CONTENT_TYPE, SCRIPT_NAME, QUERY_STRING, HTTP_*...). Il padre scrive i 7 byte sullo stdin del programma e chiude la pipe, legge lo stdout fino a EOF, attende il figlio. Il programma legge 7 byte, scrive Content-Type: ..., riga vuota, corpo; il server aggiunge status line, Date, Content-Length e risponde al client.
2. Perché il padre deve chiudere le estremità inutilizzate delle pipe? Cosa succede se non lo fa?
Traccia. EOF su una pipe si ha solo quando tutte le copie dell'estremità di scrittura sono chiuse. Se il padre tiene aperto outp[1], la sua read(outp[0]) non ritorna mai 0 e il server si blocca anche dopo che il programma è terminato; se non chiude inp[1] dopo il corpo, il programma che attende EOF su stdin non termina.
3. Elencare tre vulnerabilità di un'applicazione CGI e le contromisure.
Traccia. Command injection (parametri in system()): usare exec con argomenti e convalidare. Path traversal (nomi con ..): accettare solo nomi sicuri. Buffer overflow su QUERY_STRING: controllare le lunghezze. In più: Shellshock/HTTP_* (non usare bash vecchie, ambiente minimo), httpoxy (non passare Proxy), DoS (timeout e limiti).
Versione ripasso
- CGI (RFC 3875). Interfaccia server web <-> programma esterno che genera la risposta. Per ogni richiesta:
fork+execdel programma; dati in variabili d'ambiente e stdin; risposta su stdout; il server la trasforma in HTTP. Qualunque linguaggio; costo: un processo per richiesta. - Meta-variabili.
REQUEST_METHOD,QUERY_STRING(non decodificata, vuota se assente),CONTENT_LENGTH,CONTENT_TYPE,SCRIPT_NAME,PATH_INFO(decodificato),SERVER_NAME/SERVER_PORT/SERVER_PROTOCOL/SERVER_SOFTWARE,GATEWAY_INTERFACE=CGI/1.1,REMOTE_ADDR,AUTH_TYPE/REMOTE_USER,HTTP_*(header in maiuscolo,-->_).Content-LengtheContent-Typenon diventanoHTTP_*./cgi-bin/somma/extra?a=3&b=4:SCRIPT_NAME=/cgi-bin/somma,PATH_INFO=/extra,QUERY_STRING=a=3&b=4. - GET e POST. GET: dati in
QUERY_STRING. POST: dati su stdin, leggere esattamenteCONTENT_LENGTHbyte (mai fino a EOF). Moduli:nome=valore&...in percent-encoding,+= spazio, da decodificare nel programma (Caching, autenticazione, tipi MIME e URI in HTTPLa cache HTTP riusa le risposte senza contattare il server finche' sono fresche (Cache-Control: max-age, Expires; in mancanza una durata euristica pari a circa il 10% del tempo trascorso da Last-Modified) e, quando sono scadute, le rivalida con una richiesta condizionale (If-None-Match con ETag, If-Modified-Since con Last-Modified) alla quale il server risponde 304 senza corpo; no-store vieta di memorizzare, no-cache obbliga a rivalidare, private esclude le cache condivise. L'autenticazione usa 401 con WWW-Authenticate e la risposta Authorization: Basic (base64 di utente:password, solo sopra TLS) o Digest (hash con nonce); 407 vale per i proxy. Content-Type porta il tipo MIME (tipo/sottotipo; parametri come charset e boundary), Accept e gli altri header Accept-* guidano la negoziazione. Un URI (RFC 3986) e' scheme://userinfo@host:porta/percorso?query#frammento, con percent-encoding %HH per i byte non ammessi e regole per risolvere i riferimenti relativi.Caching, autenticazione, tipi MIME e URI in HTTP →). - Risposta. Header CGI, riga vuota, corpo.
Content-Typeobbligatorio con un corpo;Status: 404 Not Found(default 200);Location+ URI assoluto =302;Locationcon solo il percorso = redirect locale (il server rielabora). Il server aggiunge status line,Date,Server,Content-Length,Connection. Esempio: corpo3 + 4 = 7\n= 10 byte. NPH: output già HTTP completo. Errore del programma:500/502.
pipe(inp); pipe(outp);
if (fork() == 0) { /* figlio */
dup2(inp[0], 0); dup2(outp[1], 1);
close(inp[0]); close(inp[1]); close(outp[0]); close(outp[1]);
alarm(10); execle(script, name, (char *)NULL, envp); _exit(127);
}
close(inp[0]); close(outp[1]); /* padre */
write(inp[1], body, clen); close(inp[1]); /* EOF per il programma */
/* read(outp[0]) fino a 0, poi waitpid */- Dettagli.
dup2prima diexec; chiudere le estremità inutilizzate (altrimenti nessun EOF: il server si blocca);waitpid(niente zombie);alarmprima diexec(sopravvive); tetto a input e output; deadlock se si scrive molto corpo e il programma scrive molto output (pipe da 64 KiB): corpo limitato opoll; ambiente costruito dal server, non quello del server. - Sicurezza. Command injection (
system()con parametri), path traversal (nomi sicuri: lettere, cifre,_ - .), buffer overflow suQUERY_STRING, header injection (\r\nnei valori), Shellshock (valoriHTTP_*interpretati da bash), httpoxy (headerProxy->HTTP_PROXY: non passarlo), DoS (timeout, limiti), privilegi minimi. - Alternative. FastCGI (processo persistente via socket), moduli del server (
mod_php), server applicativi (WSGI, servlet, Node, Go) con reverse proxy (DNS, proxy web, HTTP CONNECT e gateway applicativiUn programma risolve i nomi con getaddrinfo (file hosts e poi DNS); un messaggio DNS (RFC 1035) ha un header di 12 byte (ID, flag QR/RD/RA/TC e RCODE, quattro contatori), una domanda (QNAME a etichette con lunghezza, QTYPE, QCLASS) e record di risposta con nomi eventualmente compressi da puntatori, su UDP porta 53 con ripiego su TCP se il bit TC e' acceso; un web proxy e' un intermediario scelto dal client che riceve richieste con URI assoluto, le inoltra al server (togliendo gli header hop-by-hop, aggiungendo Via e X-Forwarded-For) e puo' memorizzare le risposte; le cache si organizzano in gerarchie e con funzioni hash coerenti, le CDN le replicano vicino agli utenti; il metodo CONNECT chiede al proxy un tunnel TCP verso host:porta e dopo la risposta 2xx il proxy inoltra i byte in entrambe le direzioni senza interpretarli (HTTPS, con restrizione delle porte); un gateway o reverse proxy e' un intermediario scelto dal server che traduce o inoltra le richieste ad altri sistemi (bilanciamento, terminazione TLS, FastCGI, API gateway).DNS, proxy web, HTTP CONNECT e gateway applicativi →). - Codice essenziale (le funzioni centrali, senza commenti):
static long param(const char *qs, const char *name)
{
size_t k = strlen(name);
for (const char *p = qs; p != NULL && *p; ) {
if (strncmp(p, name, k) == 0 && p[k] == '=')
return strtol(p + k + 1, NULL, 10);
p = strchr(p, '&');
if (p != NULL)
p++;
}
return 0;
}
int main(void)
{
const char *method = getenv("REQUEST_METHOD");
char data[1024] = "";
if (method != NULL && strcmp(method, "POST") == 0) {
const char *cl = getenv("CONTENT_LENGTH");
size_t n = cl ? (size_t)atol(cl) : 0;
if (n >= sizeof data)
n = sizeof data - 1;
size_t got = fread(data, 1, n, stdin);
data[got] = '\0';
} else {
const char *qs = getenv("QUERY_STRING");
snprintf(data, sizeof data, "%s", qs ? qs : "");
}
long a = param(data, "a"), b = param(data, "b");
printf("Content-Type: text/plain; charset=utf-8\r\n");
printf("\r\n");
printf("%ld + %ld = %ld\n", a, b, a + b);
return 0;
}- Errori tipici: manca la riga vuota o
Content-Type; stdin letto fino a EOF; pipe non chiuse; nientewaitpido timeout;system()con dati dell'utente;QUERY_STRINGcreduta decodificata; codice dopoexecsenza_exit.