Esercizio - Server HTTP con CGI (sul modello della prova pratica)
In questa pagina 6
Testo (sul modello della prova pratica di Reti di Calcolatori, Ing. Informatica UniPD).
Scrivere in C un server HTTP che esegue programmi CGI (RFC 3875). Il server riceve porta e cartella radice; i programmi stanno in radice/cgi-bin/. Per una richiesta GET o POST a /cgi-bin/NOME[/PATH_INFO][?QUERY] il server deve:
- verificare che
NOMEsia un file eseguibile nella cartellacgi-bin(404se non esiste,403se non è eseguibile; nomi con caratteri non sicuri e..non ammessi); - lanciare il programma con
fork, duepipe,dup2edexec, passandogli le meta-variabili CGI nell'ambiente:REQUEST_METHOD,QUERY_STRING,CONTENT_LENGTH,CONTENT_TYPE,SCRIPT_NAME,PATH_INFO,SERVER_NAME,SERVER_PORT,SERVER_PROTOCOL,SERVER_SOFTWARE,GATEWAY_INTERFACE,REMOTE_ADDRe gli header della richiesta comeHTTP_*; - scrivere sullo standard input del programma il corpo della
POST(CONTENT_LENGTHbyte); - leggere tutto lo standard output del programma e interpretarlo (header CGI:
Content-Type,Status,Location; riga vuota; corpo), costruendo la risposta HTTP conContent-Length; - gestire gli errori: programma che fallisce o produce un'uscita non valida (
502), che si blocca (timeout di 10 s), corpo troppo grande (413),POSTsenzaContent-Length(411).
Scrivere anche due programmi CGI di prova: uno in shell e uno in C.
Teoria: CGI e applicazioni web dinamicheLa Common Gateway Interface (CGI, RFC 3875) e' l'interfaccia fra un server web e un programma esterno che genera la risposta: per ogni richiesta il server fa fork ed exec del programma, gli passa i dati con le meta-variabili d'ambiente (REQUEST_METHOD, QUERY_STRING, CONTENT_LENGTH, CONTENT_TYPE, SCRIPT_NAME, PATH_INFO, SERVER_*, REMOTE_ADDR, HTTP_* per gli header) e con lo standard input (corpo della POST, CONTENT_LENGTH byte); il programma scrive sullo standard output header CGI (Content-Type obbligatorio, Status, Location), una riga vuota e il corpo, e il server li trasforma in una risposta HTTP completa; i punti delicati sono pipe e dup2, la chiusura delle estremita' inutilizzate, il timeout, i limiti di dimensione e la sicurezza (injection, path traversal, header Proxy, shellshock); il costo di un processo per richiesta ha portato a FastCGI, ai moduli del server e ai server applicativi.CGI e applicazioni web dinamiche →, 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 →, HTTP 1.1 - connessioni persistenti, Content-Length e chunked transfer encodingHTTP/1.1 (oggi RFC 9110 e 9112) rende la connessione persistente di default (si chiude solo con "Connection: close"), rende obbligatorio l'header Host (virtual hosting) e introduce i nuovi metodi PUT, DELETE, OPTIONS, TRACE, Expect: 100-continue, richieste di intervalli (206) e Transfer-Encoding: chunked; con la connessione persistente il client deve sapere dove finisce ogni risposta: lunghezza del corpo nell'ordine HEAD/1xx/204/304 senza corpo, Transfer-Encoding chunked, Content-Length, altrimenti fino alla chiusura; il chunked divide il corpo in blocchi preceduti dalla lunghezza in esadecimale, termina con un chunk 0 e un trailer facoltativo, e si decodifica contando i byte dichiarati (non cercando CRLF).HTTP 1.1 - connessioni persistenti, Content-Length e chunked transfer encoding →, 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 →.
Idea
Il server è iterativo e riusa l'ossatura degli esercizi precedenti (Esercizio - Server HTTP iterativo con GET, HEAD e codici di errore (sul modello della prova pratica)): lettore a buffer, request line, header, ciclo di accept. Cambia che cosa si fa con la richiesta: invece di aprire un file, si esegue un programma.
client ---HTTP---> server --pipe stdin--> programma CGI (corpo della POST, CONTENT_LENGTH byte)
<---HTTP--- <--pipe stdout-- (header CGI + riga vuota + corpo)
ambiente: meta-variabiliFasi di handle:
- Request line e header: si memorizza ogni header (nome e valore) per costruire
HTTP_*; si tengono a parteContent-Length,Content-Type,Host. - Controlli: metodo
GET/POST/HEAD;411sePOSTsenzaContent-Lengtho conTransfer-Encoding(un corpo CGI ha bisogno diCONTENT_LENGTH);413se il corpo supera 60 000 byte. - Instradamento:
/cgi-bin/NOME[/PATH_INFO]: il nome è fino al secondo/, il resto èPATH_INFO(che il server decodifica, mentreQUERY_STRINGresta grezza); il nome ammette solo lettere, cifre,_,-,., e non inizia con.: così non si può uscire dacgi-bin. - Corpo: lettura di esattamente
CONTENT_LENGTHbyte conrb_readn. - Ambiente:
add_envcostruisceenvpin un'area statica. run_cgi: pipe,fork,dup2,exec(sotto).cgi_parse: interpreta l'uscita esend_responseproduce la risposta.
Codice
Il server
/* cgi_server.c - server HTTP che esegue programmi CGI (RFC 3875): GET e POST su /cgi-bin/NOME.
*
* Per ogni richiesta: fork + pipe + exec del programma, con le meta-variabili CGI nell'ambiente e il corpo
* della POST sullo standard input; l'uscita del programma (header CGI, riga vuota, corpo) diventa la risposta HTTP.
*
* Compilare: gcc -Wall -Wextra -o cgi_server cgi_server.c
* Avviare: ./cgi_server 8080 ./www (i programmi stanno in ./www/cgi-bin, con il permesso di esecuzione)
* Provare: curl -i "http://127.0.0.1:8080/cgi-bin/hello.sh?nome=Mario"
* curl -i -d "a=3&b=4" http://127.0.0.1:8080/cgi-bin/somma (POST)
* curl -i http://127.0.0.1:8080/cgi-bin/vai.sh (script che manda Location)
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <ctype.h>
#include <errno.h>
#include <time.h>
#include <signal.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define MAX_LINE 2048
#define MAX_BODY 60000 /* < 64 KiB (capienza di una pipe): scrivere il corpo non puo' bloccarsi */
#define MAX_OUTPUT (4 * 1024 * 1024)
#define CGI_TIMEOUT 10 /* secondi: poi il programma viene ucciso da SIGALRM */
static const char *root_dir;
static const char *port_str;
static int write_all(int fd, const char *buf, size_t n)
{
while (n > 0) {
ssize_t w = write(fd, buf, n);
if (w < 0) {
if (errno == EINTR)
continue;
return -1;
}
buf += w;
n -= (size_t)w;
}
return 0;
}
/* ---------- lettore con buffer ---------- */
struct rbuf {
int fd;
char buf[4096];
size_t pos, len;
};
static int rb_fill(struct rbuf *r)
{
while (r->pos == r->len) {
ssize_t k = read(r->fd, r->buf, sizeof r->buf);
if (k < 0 && errno == EINTR)
continue;
if (k <= 0)
return -1;
r->pos = 0;
r->len = (size_t)k;
}
return 0;
}
static int rb_line(struct rbuf *r, char *line, size_t max)
{
size_t n = 0;
for (;;) {
if (rb_fill(r) < 0)
return -1;
char c = r->buf[r->pos++];
if (c == '\n') {
if (n > 0 && line[n - 1] == '\r')
n--;
line[n] = '\0';
return (int)n;
}
if (n + 1 >= max)
return -2;
line[n++] = c;
}
}
static int rb_readn(struct rbuf *r, char *dst, size_t n) /* esattamente n byte */
{
while (n > 0) {
if (rb_fill(r) < 0)
return -1;
size_t take = r->len - r->pos < n ? r->len - r->pos : n;
memcpy(dst, r->buf + r->pos, take);
r->pos += take;
dst += take;
n -= take;
}
return 0;
}
/* ---------- ambiente del programma CGI ---------- */
static char envpool[16384];
static size_t envused;
static char *envp[96];
static int nenv;
static void add_env(const char *name, const char *value)
{
size_t need = strlen(name) + strlen(value) + 2;
if (nenv >= 95 || envused + need > sizeof envpool)
return; /* pieno: la variabile si perde (limite voluto) */
char *dst = envpool + envused;
snprintf(dst, need, "%s=%s", name, value);
envp[nenv++] = dst;
envused += need;
}
/* ---------- risposta del programma CGI ---------- */
struct cgi_resp {
int status;
char reason[64];
char ctype[256];
char extra[2048]; /* altri header da inoltrare (Set-Cookie, Location...) */
const char *body;
size_t body_len;
};
static const char *reason_of(int code)
{
switch (code) {
case 200: return "OK";
case 201: return "Created";
case 204: return "No Content";
case 302: return "Found";
case 400: return "Bad Request";
case 403: return "Forbidden";
case 404: return "Not Found";
case 405: return "Method Not Allowed";
case 411: return "Length Required";
case 413: return "Content Too Large";
case 500: return "Internal Server Error";
case 502: return "Bad Gateway";
case 504: return "Gateway Timeout";
default: return "Status";
}
}
/* Separa header e corpo dell'uscita del programma. Torna 0 se valida.
* Header CGI: Content-Type, Status ("Status: 404 Not Found"), Location, altri. Riga vuota. Corpo. */
static int cgi_parse(const char *out, size_t len, struct cgi_resp *r)
{
memset(r, 0, sizeof *r);
r->status = 0;
size_t pos = 0;
int have_location = 0;
for (;;) {
const char *nl = memchr(out + pos, '\n', len - pos);
if (nl == NULL)
return -1; /* niente riga vuota: uscita malformata */
size_t ll = (size_t)(nl - (out + pos));
char line[1024];
size_t cl = ll > 0 && out[pos + ll - 1] == '\r' ? ll - 1 : ll; /* senza CR */
if (cl >= sizeof line)
return -1;
memcpy(line, out + pos, cl);
line[cl] = '\0';
pos += ll + 1;
if (cl == 0)
break; /* riga vuota: finiscono gli header */
char *colon = strchr(line, ':');
if (colon == NULL)
return -1;
*colon = '\0';
const char *v = colon + 1;
while (*v == ' ' || *v == '\t')
v++;
if (strcasecmp(line, "Status") == 0) {
r->status = atoi(v);
const char *sp = strchr(v, ' ');
snprintf(r->reason, sizeof r->reason, "%s", sp ? sp + 1 : reason_of(r->status));
} else if (strcasecmp(line, "Content-Type") == 0) {
snprintf(r->ctype, sizeof r->ctype, "%s", v);
} else {
if (strcasecmp(line, "Location") == 0)
have_location = 1;
size_t used = strlen(r->extra);
snprintf(r->extra + used, sizeof r->extra - used, "%s: %s\r\n", line, v);
}
}
r->body = out + pos;
r->body_len = len - pos;
if (r->status == 0) {
r->status = have_location ? 302 : 200; /* default CGI: 200, o 302 se c'e' solo Location */
snprintf(r->reason, sizeof r->reason, "%s", reason_of(r->status));
}
if (r->ctype[0] == '\0' && r->body_len > 0)
return -1; /* con un corpo il Content-Type e' obbligatorio */
if (r->status < 100 || r->status > 599)
return -1;
return 0;
}
static void http_date(char *out, size_t size)
{
time_t t = time(NULL);
strftime(out, size, "%a, %d %b %Y %H:%M:%S GMT", gmtime(&t));
}
static void send_response(int fd, int status, const char *reason, const char *ctype, const char *extra,
const char *body, size_t blen, int head_only)
{
char head[4096], date[64];
http_date(date, sizeof date);
int n = snprintf(head, sizeof head,
"HTTP/1.1 %d %s\r\nDate: %s\r\nServer: mini-cgi/1.0\r\n", status, reason, date);
if (ctype && ctype[0])
n += snprintf(head + n, sizeof head - (size_t)n, "Content-Type: %s\r\n", ctype);
n += snprintf(head + n, sizeof head - (size_t)n, "%sContent-Length: %zu\r\nConnection: close\r\n\r\n", extra, blen);
write_all(fd, head, (size_t)n);
if (!head_only && blen > 0)
write_all(fd, body, blen);
}
static void send_error(int fd, int status)
{
char body[128];
int n = snprintf(body, sizeof body, "%d %s\n", status, reason_of(status));
send_response(fd, status, reason_of(status), "text/plain", "", body, (size_t)n, 0);
}
/* ---------- esecuzione del programma ---------- */
/* Esegue script con l'ambiente envp e 'input' sullo stdin; raccoglie l'uscita in *out (malloc). Torna 0 se ok,
* -1 errore di sistema, -2 uscita troppo lunga, -3 il programma e' fallito (codice diverso da 0 o ucciso). */
static int run_cgi(const char *script, const char *dir, const char *input, size_t ilen, char **out, size_t *olen)
{
int inp[2], outp[2];
if (pipe(inp) < 0)
return -1;
if (pipe(outp) < 0) {
close(inp[0]);
close(inp[1]);
return -1;
}
const char *name = strrchr(script, '/') + 1;
char local[256];
snprintf(local, sizeof local, "./%s", name); /* dopo chdir(dir) il programma sta in "./nome" */
pid_t pid = fork();
if (pid < 0) {
close(inp[0]); close(inp[1]); close(outp[0]); close(outp[1]);
return -1;
}
if (pid == 0) {
/* FIGLIO: stdin <- pipe in, stdout -> pipe out. stderr resta quello del server (log). */
dup2(inp[0], STDIN_FILENO);
dup2(outp[1], STDOUT_FILENO);
for (int fd = 3; fd < 256; fd++) /* chiude pipe, socket di ascolto e socket del client ereditati */
close(fd);
signal(SIGPIPE, SIG_DFL); /* "ignorato" resta tale dopo exec: il programma riavra' il default */
if (chdir(dir) < 0)
_exit(126);
alarm(CGI_TIMEOUT); /* il timer sopravvive a exec: un programma bloccato viene ucciso */
execle(local, name, (char *)NULL, envp);
_exit(127); /* exec fallita */
}
/* PADRE: chiude le estremita' che non usa, altrimenti non vedrebbe mai EOF. */
close(inp[0]);
close(outp[1]);
if (ilen > 0)
write_all(inp[1], input, ilen); /* corpo della POST: sta tutto nella pipe (limite MAX_BODY) */
close(inp[1]); /* il programma vede EOF su stdin */
size_t cap = 8192, len = 0;
char *buf = malloc(cap);
int rc = 0;
for (;;) {
if (len == cap) {
if (cap >= MAX_OUTPUT) {
rc = -2;
break;
}
cap *= 2;
char *nb = realloc(buf, cap);
if (nb == NULL) {
rc = -1;
break;
}
buf = nb;
}
ssize_t r = read(outp[0], buf + len, cap - len);
if (r < 0 && errno == EINTR)
continue;
if (r <= 0)
break; /* EOF: il programma ha chiuso stdout (di solito, e' terminato) */
len += (size_t)r;
}
close(outp[0]);
int status = 0;
waitpid(pid, &status, 0); /* raccoglie il figlio: niente zombie */
if (rc == 0 && (!WIFEXITED(status) || WEXITSTATUS(status) != 0))
rc = -3;
if (rc != 0 && rc != -3) {
free(buf);
return rc;
}
*out = buf;
*olen = len;
return rc;
}
static int name_ok(const char *s) /* solo lettere, cifre, '_', '-', '.'; non inizia con '.' */
{
if (*s == '\0' || *s == '.')
return 0;
for (; *s; s++)
if (!isalnum((unsigned char)*s) && *s != '_' && *s != '-' && *s != '.')
return 0;
return 1;
}
/* PATH_INFO, a differenza di QUERY_STRING, arriva al programma DECODIFICATO (RFC 3875 par. 4.1.5): "%20" -> ' '.
* Torna -1 se una sequenza e' rotta o se contiene un byte nullo o una '/' codificata (%2F): perderebbe informazione. */
static int pct_decode(const char *src, char *dst, size_t size)
{
size_t n = 0;
for (; *src; src++) {
int c = (unsigned char)*src;
if (c == '%') {
if (!isxdigit((unsigned char)src[1]) || !isxdigit((unsigned char)src[2]))
return -1;
char hex[3] = {src[1], src[2], '\0'};
c = (int)strtol(hex, NULL, 16);
src += 2;
if (c == 0 || c == '/')
return -1;
}
if (n + 1 >= size)
return -1;
dst[n++] = (char)c;
}
dst[n] = '\0';
return 0;
}
static void handle(int fd, const char *peer)
{
struct rbuf in = {.fd = fd};
char line[MAX_LINE], method[16], target[MAX_LINE], ver[16];
int len = rb_line(&in, line, sizeof line);
if (len < 0 || sscanf(line, "%15s %2047s %15s", method, target, ver) != 3 || strncmp(ver, "HTTP/1.", 7) != 0) {
send_error(fd, 400);
return;
}
/* header: il resto diventa HTTP_* nell'ambiente */
nenv = 0;
envused = 0;
long clen = -1;
char ctype[256] = "", host[256] = "localhost";
int nh = 0, chunked = 0;
char hdrs[32][2][512]; /* header ricevuti, per costruire HTTP_* dopo */
int nhdr = 0;
for (;;) {
len = rb_line(&in, line, sizeof line);
if (len < 0 || ++nh > 100) {
send_error(fd, 400);
return;
}
if (len == 0)
break;
char *colon = strchr(line, ':');
if (colon == NULL) {
send_error(fd, 400);
return;
}
*colon = '\0';
const char *v = colon + 1;
while (*v == ' ' || *v == '\t')
v++;
if (strcasecmp(line, "Content-Length") == 0)
clen = atol(v);
else if (strcasecmp(line, "Content-Type") == 0)
snprintf(ctype, sizeof ctype, "%s", v);
else if (strcasecmp(line, "Host") == 0)
snprintf(host, sizeof host, "%s", v);
else if (strcasecmp(line, "Transfer-Encoding") == 0)
chunked = 1;
if (nhdr < 32) {
snprintf(hdrs[nhdr][0], sizeof hdrs[0][0], "%s", line);
snprintf(hdrs[nhdr][1], sizeof hdrs[0][1], "%s", v);
nhdr++;
}
}
int is_post = strcmp(method, "POST") == 0;
int head_only = strcmp(method, "HEAD") == 0;
if (!is_post && strcmp(method, "GET") != 0 && !head_only) {
send_error(fd, 405);
return;
}
if (chunked || (is_post && clen < 0)) {
send_error(fd, 411); /* un corpo CGI ha bisogno di CONTENT_LENGTH */
return;
}
if (clen > MAX_BODY) {
send_error(fd, 413);
return;
}
/* target = /cgi-bin/NOME[/PATH_INFO][?QUERY] */
char *query = strchr(target, '?');
if (query)
*query++ = '\0';
if (strncmp(target, "/cgi-bin/", 9) != 0) {
send_error(fd, 404);
return;
}
char name[128];
const char *after = target + 9;
size_t nl = strcspn(after, "/");
if (nl == 0 || nl >= sizeof name) {
send_error(fd, 404);
return;
}
memcpy(name, after, nl);
name[nl] = '\0';
char path_info[MAX_LINE]; /* "" oppure "/resto", decodificato */
if (!name_ok(name)) {
send_error(fd, 404);
return;
}
if (pct_decode(after + nl, path_info, sizeof path_info) < 0) {
send_error(fd, 400);
return;
}
char dir[MAX_LINE], script[MAX_LINE];
snprintf(dir, sizeof dir, "%s/cgi-bin", root_dir);
snprintf(script, sizeof script, "%s/%s", dir, name);
struct stat st;
if (stat(script, &st) < 0 || !S_ISREG(st.st_mode)) {
send_error(fd, 404);
return;
}
if (access(script, X_OK) < 0) {
send_error(fd, 403);
return;
}
/* corpo della POST */
char *body = NULL;
if (clen > 0) {
body = malloc((size_t)clen);
if (body == NULL || rb_readn(&in, body, (size_t)clen) < 0) {
free(body);
send_error(fd, 400);
return;
}
}
/* meta-variabili (RFC 3875 par. 4.1) */
char tmp[32];
add_env("GATEWAY_INTERFACE", "CGI/1.1");
add_env("SERVER_SOFTWARE", "mini-cgi/1.0");
add_env("SERVER_PROTOCOL", ver);
char *hc = (host[0] == '[') ? strchr(host, ']') : strrchr(host, ':'); /* SERVER_NAME e' il nome senza ":porta" */
if (hc != NULL) {
if (host[0] == '[')
hc[1] = '\0';
else
*hc = '\0';
}
add_env("SERVER_NAME", host);
add_env("SERVER_PORT", port_str);
add_env("REQUEST_METHOD", method);
char sname[160];
snprintf(sname, sizeof sname, "/cgi-bin/%s", name);
add_env("SCRIPT_NAME", sname);
add_env("PATH_INFO", path_info);
add_env("QUERY_STRING", query ? query : ""); /* grezza: la decodifica %XX spetta al programma */
add_env("REMOTE_ADDR", peer);
if (clen >= 0) {
snprintf(tmp, sizeof tmp, "%ld", clen);
add_env("CONTENT_LENGTH", tmp);
}
if (ctype[0])
add_env("CONTENT_TYPE", ctype);
for (int i = 0; i < nhdr; i++) { /* gli altri header -> HTTP_NOME */
const char *h = hdrs[i][0];
if (!strcasecmp(h, "Content-Length") || !strcasecmp(h, "Content-Type") || !strcasecmp(h, "Host") ||
!strcasecmp(h, "Authorization") || /* le credenziali non si passano ai programmi */
!strcasecmp(h, "Proxy")) /* "Proxy" -> HTTP_PROXY ingannerebbe le librerie HTTP (httpoxy) */
continue;
char vname[600] = "HTTP_";
size_t k = 5;
for (const char *c = h; *c && k < sizeof vname - 1; c++)
vname[k++] = *c == '-' ? '_' : (char)toupper((unsigned char)*c);
vname[k] = '\0';
add_env(vname, hdrs[i][1]);
}
envp[nenv] = NULL;
char *out = NULL;
size_t olen = 0;
int rc = run_cgi(script, dir, body, clen > 0 ? (size_t)clen : 0, &out, &olen);
free(body);
if (rc == -2 || rc == -1) {
send_error(fd, 502);
return;
}
struct cgi_resp cr;
if (rc == -3 && olen == 0) {
free(out);
send_error(fd, 502); /* il programma e' fallito senza produrre nulla */
return;
}
if (cgi_parse(out, olen, &cr) < 0) {
fprintf(stderr, "%s: uscita CGI non valida da %s\n", peer, name);
free(out);
send_error(fd, 502); /* risposta incomprensibile dal programma: Bad Gateway */
return;
}
send_response(fd, cr.status, cr.reason, cr.ctype, cr.extra, cr.body, cr.body_len, head_only);
fprintf(stderr, "%s \"%s %s\" -> %d (CGI %s)\n", peer, method, target, cr.status, name);
free(out);
}
int main(int argc, char **argv)
{
if (argc != 3) {
fprintf(stderr, "uso: %s porta radice\n", argv[0]);
return 1;
}
port_str = argv[1];
root_dir = argv[2];
signal(SIGPIPE, SIG_IGN);
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if (lfd < 0) {
perror("socket");
return 1;
}
int yes = 1;
setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof yes);
struct sockaddr_in addr;
memset(&addr, 0, sizeof addr);
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_port = htons((unsigned short)atoi(port_str));
if (bind(lfd, (struct sockaddr *)&addr, sizeof addr) < 0 || listen(lfd, 16) < 0) {
perror("bind/listen");
return 1;
}
for (;;) {
struct sockaddr_in cli;
socklen_t clen = sizeof cli;
int cfd = accept(lfd, (struct sockaddr *)&cli, &clen);
if (cfd < 0) {
if (errno == EINTR)
continue;
perror("accept");
break;
}
char peer[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &cli.sin_addr, peer, sizeof peer);
handle(cfd, peer); /* iterativo: il CGI gira in un figlio, il server aspetta */
close(cfd);
}
return 0;
}Programmi CGI di prova
Un programma in shell (con #!/bin/sh all'inizio: senza, exec fallisce con ENOEXEC):
#!/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 con GET o POST:
/* 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;
}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 ""Compilare e provare
$ mkdir -p www/cgi-bin
$ cp hello.sh vai.sh www/cgi-bin/ && chmod +x www/cgi-bin/hello.sh www/cgi-bin/vai.sh
$ gcc -Wall -Wextra -o www/cgi-bin/somma somma.c
$ gcc -Wall -Wextra -o cgi_server cgi_server.c
$ ./cgi_server 8080 ./www &Il risultato atteso (Date dipende dal momento e HTTP_USER_AGENT dal client; con curl/8.5.0 il corpo è di 204 byte):
$ curl -i "http://127.0.0.1:8080/cgi-bin/hello.sh?nome=Mario"
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: 204
Connection: close
Ciao dal CGI!
REQUEST_METHOD = GET
QUERY_STRING = nome=Mario
SCRIPT_NAME = /cgi-bin/hello.sh
PATH_INFO =
SERVER_PROTOCOL = HTTP/1.1
REMOTE_ADDR = 127.0.0.1
HTTP_USER_AGENT = curl/8.5.0
$ curl "http://127.0.0.1:8080/cgi-bin/somma?a=3&b=4" # GET: dati in QUERY_STRING
3 + 4 = 7
$ curl -i -d "a=30&b=12" http://127.0.0.1:8080/cgi-bin/somma # POST: dati su stdin
HTTP/1.1 200 OK ...
30 + 12 = 42
$ curl -i http://127.0.0.1:8080/cgi-bin/vai.sh # solo Location: il server risponde 302
HTTP/1.1 302 Found
Location: http://127.0.0.1:8080/cgi-bin/hello.sh?da=vai
Content-Length: 0
$ curl -i http://127.0.0.1:8080/cgi-bin/nonesiste # 404
$ curl -i "http://127.0.0.1:8080/cgi-bin/hello.sh/extra%20path" # PATH_INFO = /extra path (decodificato)Prove sugli errori:
$ printf '#!/bin/sh\nexit 3\n' > www/cgi-bin/fallisce.sh && chmod +x www/cgi-bin/fallisce.sh
$ curl -i http://127.0.0.1:8080/cgi-bin/fallisce.sh # nessuna uscita e codice 3: 502 Bad Gateway
$ printf '#!/bin/sh\nsleep 30\n' > www/cgi-bin/lento.sh && chmod +x www/cgi-bin/lento.sh
$ time curl -i http://127.0.0.1:8080/cgi-bin/lento.sh # dopo 10 s SIGALRM uccide il programma: 502
$ printf '#!/bin/sh\necho "senza header"\n' > www/cgi-bin/rotto.sh && chmod +x www/cgi-bin/rotto.sh
$ curl -i http://127.0.0.1:8080/cgi-bin/rotto.sh # uscita senza riga vuota/Content-Type: 502
$ chmod -x www/cgi-bin/hello.sh; curl -i http://127.0.0.1:8080/cgi-bin/hello.sh # 403
$ curl -i "http://127.0.0.1:8080/cgi-bin/%2e%2e/x" # 404 (il nome contiene '%')
$ printf 'POST /cgi-bin/somma HTTP/1.1\r\nHost: x\r\n\r\n' | nc 127.0.0.1 8080 | head -1 # POST senza Content-Length: 411 Length RequiredSpiegazione dei punti chiave
Le meta-variabili (add_env). envp è un array di puntatori a stringhe NOME=valore, terminato da NULL, costruito in un'area statica envpool. Ogni chiamata aggiunge una voce e ignora quelle che non entrano (limite voluto). Variabili:
GATEWAY_INTERFACE=CGI/1.1,SERVER_SOFTWARE,SERVER_PROTOCOL(la versione della richiesta);SERVER_NAME: il nome dall'headerHostsenza la porta (si taglia all'ultimo:; per un IPv6 fra parentesi si taglia dopo]);SERVER_PORTè la porta di ascolto;REQUEST_METHOD,SCRIPT_NAME=/cgi-bin/NOME,PATH_INFO(decodificato conpct_decode: rifiuta%2F,%00e sequenze rotte con400),QUERY_STRING(grezza: la decodifica spetta al programma),REMOTE_ADDR;CONTENT_LENGTHeCONTENT_TYPEsolo se presenti;- per ogni altro header,
HTTP_+ il nome in maiuscolo con-sostituito da_(User-Agent→HTTP_USER_AGENT). Si escludonoAuthorization(le credenziali non si passano al programma),Hoste, soprattutto,Proxy: l'headerProxy: ...diventerebbeHTTP_PROXY, e molte librerie HTTP lo usano come proxy di uscita (vulnerabilità httpoxy).
run_cgi.
padre: pipe(inp); pipe(outp); fork ─────────────► figlio: dup2(inp[0],0); dup2(outp[1],1);
close(3..255); signal(SIGPIPE,SIG_DFL);
chdir(cgi-bin); alarm(10); execle("./nome", ...)
close(inp[0]); close(outp[1]);
write(inp[1], corpo); close(inp[1]); ← il programma vede EOF su stdin
read(outp[0]) fino a EOF; waitpid- Le due pipe sono i canali:
inp(server → programma, diventa stdin) eoutp(programma → server, diventa stdout). Ogni pipe ha una estremità di lettura[0]e una di scrittura[1]. dup2(inp[0], STDIN_FILENO)edup2(outp[1], STDOUT_FILENO)collegano i descrittori standard alle pipe prima diexec: il programma eseguito li eredita e non sa nulla di pipe.- Il figlio chiude tutti i descrittori da 3 in su: le pipe originali, ma anche il socket di ascolto e quello del client, che il programma non deve ereditare.
signal(SIGPIPE, SIG_DFL): il server ignoraSIGPIPE, e una disposizione "ignora" sopravvive aexec: il programma CGI deve ricevere il comportamento normale.chdir(dir): il programma gira nella propria cartella (come richiesto dalla prassi CGI); per questo dopochdiril percorso da eseguire è"./nome", non quello relativo alla radice (un errore sottile conroot_dirrelativa come./www: dopo ilchdiril percorso./www/cgi-bin/nomenon esisterebbe più).alarm(CGI_TIMEOUT)prima diexec: il timer sopravvive aexeceSIGALRMtermina il programma dopo 10 secondi: un programma bloccato non blocca il server.execle(local, name, (char *)NULL, envp):argv[0]è il nome; l'ambiente è soloenvp, non quello del server. Seexecfallisce (permessi, script senza#!), il figlio esce con_exit(127).- Il padre chiude
inp[0]eoutp[1]: sono le estremità che non usa. Chiudereoutp[1]è essenziale:read(outp[0])restituisce0(EOF) solo quando nessuno ha più aperta l'estremità di scrittura; se il padre ne tenesse una copia, la lettura non finirebbe mai. - Scrive il corpo in
inp[1]e chiude: il programma che leggestdinvede EOF. - Il limite di 60 000 byte sul corpo evita il deadlock: una pipe contiene circa 64 KiB; se il programma scrivesse molto output senza leggere stdin e il server scrivesse un corpo enorme, i due si bloccherebbero a vicenda (con corpi grandi servirebbe
poll). - Si legge l'uscita in un buffer che raddoppia (
realloc) fino aMAX_OUTPUT(4 MiB, poi502); infinewaitpidraccoglie il figlio (niente zombie) eWIFEXITED/WEXITSTATUSdicono se è terminato con codice0.
cgi_parse (interpreta l'uscita). Scorre le righe fino alla prima riga vuota (con CRLF o solo LF); Status: 404 Not Found → codice e frase; Content-Type → tipo; Location → memorizzato fra gli header extra; gli altri header sono inoltrati. Il corpo è tutto ciò che segue. Regole: senza riga vuota → non valida; con un corpo serve Content-Type; senza Status: 200, oppure 302 se c'è Location; codici fuori da 100-599 → non valida. Se non è valida, il server risponde 502 Bad Gateway (il programma, "gateway" verso il quale il server inoltra, ha dato una risposta incomprensibile).
send_response. Compone: status-line, Date, Server, Content-Type (se c'è), gli header del programma, Content-Length (la lunghezza del corpo, nota perché si è letto tutto), Connection: close, riga vuota, e il corpo (omesso per HEAD). La lunghezza nota evita il chunked.
Programmi di prova. hello.sh stampa Content-Type, una riga vuota, e le variabili: è la forma più piccola di CGI. somma.c: per GET legge QUERY_STRING; per POST legge CONTENT_LENGTH byte con fread (mai fino a EOF); estrae a e b con strncmp e strtol; stampa Content-Type: text/plain\r\n\r\n e il risultato. vai.sh costruisce un URI assoluto dalle meta-variabili SERVER_NAME e SERVER_PORT.
Errori tipici
execcon percorso relativo dopochdir: il programma non si trova (errore 127/502). Si esegue./nomenella cartella corrente.- Non chiudere
outp[1]nel padre (il server si blocca inread) oinp[1]dopo il corpo (il programma attende EOF su stdin). - Non chiudere i descrittori ereditati nel figlio (il programma tiene aperto il socket del client).
- Dimenticare
waitpid(zombie) o il timeout (un programma bloccato blocca il server). - Costruire una riga di comando con
system()e dati della richiesta: command injection; si usaexeccon argomenti separati. - Passare al programma l'ambiente del server (segreti) o l'header
Proxy(httpoxy). - Il programma legge
stdinfino a EOF: può non arrivare mai; deve leggereCONTENT_LENGTHbyte. - Il programma non stampa la riga vuota dopo gli header o dimentica
Content-Type:502. - Nomi dei programmi non controllati (
../): esecuzione di file fuori dacgi-bin. - Script senza
#!o senza il permesso di esecuzione (ENOEXEC,EACCES). - Aspettarsi
PATH_INFOancora codificato, oQUERY_STRINGgià decodificata.
Varianti per esercitarsi
- Servire anche i file statici nella stessa radice (unendo questo server a Esercizio - Server HTTP iterativo con GET, HEAD e codici di errore (sul modello della prova pratica)).
- Concorrenza: un processo per connessione (Esercizio - Server HTTP concorrente con fork e connessioni persistenti (sul modello della prova pratica)); i CGI già girano in processi figli.
- Gestire il redirect locale (
Location: /percorso) riesaminando la richiesta. - Inoltrare l'uscita mentre arriva con
Transfer-Encoding: chunked, senza accumularla (Esercizio - Server HTTP che risponde in chunked con trailer (sul modello della prova pratica)). - Usare
pollper scrivere il corpo e leggere l'uscita insieme (niente limite di 60 000 byte).
Versione ripasso
- Testo. Server
./cgi_server porta radiceche esegueradice/cgi-bin/NOMEconfork, duepipe,dup2,exec; meta-variabili nell'ambiente; corpo dellaPOSTsu stdin; uscita (header CGI, riga vuota, corpo) -> risposta HTTP conContent-Length; errori404,403,411,413,502, timeout. - Flusso di
handle. Request line + header (si tengono perHTTP_*;Content-Length,Content-Type,Hosta parte) -> metodoGET/POST/HEAD(405) ->411se POST senzaContent-Lengtho conTransfer-Encoding->413oltre 60 000 byte ->/cgi-bin/NOME[/PATH_INFO]con nome sicuro (lettere, cifre,_ - ., non.) ->stat(404) eaccess(X_OK)(403) -> corpo conrb_readn-> ambiente ->run_cgi->cgi_parse->send_response. - Ambiente.
GATEWAY_INTERFACE=CGI/1.1,SERVER_SOFTWARE,SERVER_PROTOCOL,SERVER_NAME(Host senza porta),SERVER_PORT,REQUEST_METHOD,SCRIPT_NAME,PATH_INFO(decodificato;%2F,%00rifiutati con400),QUERY_STRING(grezza),REMOTE_ADDR,CONTENT_LENGTH/CONTENT_TYPEse presenti,HTTP_NOMEper gli altri header; esclusiAuthorization,Host,Proxy(httpoxy). run_cgi.
pipe(inp); pipe(outp); local = "./" + name; /* dopo chdir il programma e' in ./nome */
if (fork() == 0) {
dup2(inp[0], 0); dup2(outp[1], 1);
for (fd = 3; fd < 256; fd++) close(fd); /* niente socket ereditati */
signal(SIGPIPE, SIG_DFL); /* "ignora" sopravvive a exec */
chdir(dir); alarm(10); /* il timer sopravvive a exec */
execle(local, name, (char *)NULL, envp); _exit(127);
}
close(inp[0]); close(outp[1]); /* estremità non usate: senza, niente EOF */
write_all(inp[1], body, clen); close(inp[1]); /* EOF per il programma */
/* read(outp[0]) in buffer che raddoppia (max 4 MiB) fino a EOF; waitpid; WIFEXITED/WEXITSTATUS */cgi_parse. Righe fino alla prima riga vuota (CRLF o LF);Status: 404 Not Found,Content-Type,Location(+ altri header inoltrati); senzaStatus:200(o302se c'èLocation); con un corpo serveContent-Type; non valida ->502.send_response: status,Date,Server, header del programma,Content-Length,Connection: close, corpo (non perHEAD).- Limiti e difese. Corpo massimo 60 000 byte (< pipe da 64 KiB: niente deadlock; con corpi grandi
poll); output massimo 4 MiB; timeout 10 s;waitpidsempre. - Prove.
curl -i ".../cgi-bin/hello.sh?nome=Mario";curl ".../cgi-bin/somma?a=3&b=4"->3 + 4 = 7;curl -d "a=30&b=12" .../somma->30 + 12 = 42;vai.sh->302conLocationassoluto; script che esce con 3 senza output o che dorme 30 s ->502;chmod -x->403. - Codice essenziale (le funzioni centrali, senza commenti):
static int run_cgi(const char *script, const char *dir, const char *input, size_t ilen, char **out, size_t *olen)
{
int inp[2], outp[2];
if (pipe(inp) < 0)
return -1;
if (pipe(outp) < 0) {
close(inp[0]);
close(inp[1]);
return -1;
}
const char *name = strrchr(script, '/') + 1;
char local[256];
snprintf(local, sizeof local, "./%s", name);
pid_t pid = fork();
if (pid < 0) {
close(inp[0]); close(inp[1]); close(outp[0]); close(outp[1]);
return -1;
}
if (pid == 0) {
dup2(inp[0], STDIN_FILENO);
dup2(outp[1], STDOUT_FILENO);
for (int fd = 3; fd < 256; fd++)
close(fd);
signal(SIGPIPE, SIG_DFL);
if (chdir(dir) < 0)
_exit(126);
alarm(CGI_TIMEOUT);
execle(local, name, (char *)NULL, envp);
_exit(127);
}
close(inp[0]);
close(outp[1]);
if (ilen > 0)
write_all(inp[1], input, ilen);
close(inp[1]);
size_t cap = 8192, len = 0;
char *buf = malloc(cap);
int rc = 0;
for (;;) {
if (len == cap) {
if (cap >= MAX_OUTPUT) {
rc = -2;
break;
}
cap *= 2;
char *nb = realloc(buf, cap);
if (nb == NULL) {
rc = -1;
break;
}
buf = nb;
}
ssize_t r = read(outp[0], buf + len, cap - len);
if (r < 0 && errno == EINTR)
continue;
if (r <= 0)
break;
len += (size_t)r;
}
close(outp[0]);
int status = 0;
waitpid(pid, &status, 0);
if (rc == 0 && (!WIFEXITED(status) || WEXITSTATUS(status) != 0))
rc = -3;
if (rc != 0 && rc != -3) {
free(buf);
return rc;
}
*out = buf;
*olen = len;
return rc;
}
static int cgi_parse(const char *out, size_t len, struct cgi_resp *r)
{
memset(r, 0, sizeof *r);
r->status = 0;
size_t pos = 0;
int have_location = 0;
for (;;) {
const char *nl = memchr(out + pos, '\n', len - pos);
if (nl == NULL)
return -1;
size_t ll = (size_t)(nl - (out + pos));
char line[1024];
size_t cl = ll > 0 && out[pos + ll - 1] == '\r' ? ll - 1 : ll;
if (cl >= sizeof line)
return -1;
memcpy(line, out + pos, cl);
line[cl] = '\0';
pos += ll + 1;
if (cl == 0)
break;
char *colon = strchr(line, ':');
if (colon == NULL)
return -1;
*colon = '\0';
const char *v = colon + 1;
while (*v == ' ' || *v == '\t')
v++;
if (strcasecmp(line, "Status") == 0) {
r->status = atoi(v);
const char *sp = strchr(v, ' ');
snprintf(r->reason, sizeof r->reason, "%s", sp ? sp + 1 : reason_of(r->status));
} else if (strcasecmp(line, "Content-Type") == 0) {
snprintf(r->ctype, sizeof r->ctype, "%s", v);
} else {
if (strcasecmp(line, "Location") == 0)
have_location = 1;
size_t used = strlen(r->extra);
snprintf(r->extra + used, sizeof r->extra - used, "%s: %s\r\n", line, v);
}
}
r->body = out + pos;
r->body_len = len - pos;
if (r->status == 0) {
r->status = have_location ? 302 : 200;
snprintf(r->reason, sizeof r->reason, "%s", reason_of(r->status));
}
if (r->ctype[0] == '\0' && r->body_len > 0)
return -1;
if (r->status < 100 || r->status > 599)
return -1;
return 0;
}
static int pct_decode(const char *src, char *dst, size_t size)
{
size_t n = 0;
for (; *src; src++) {
int c = (unsigned char)*src;
if (c == '%') {
if (!isxdigit((unsigned char)src[1]) || !isxdigit((unsigned char)src[2]))
return -1;
char hex[3] = {src[1], src[2], '\0'};
c = (int)strtol(hex, NULL, 16);
src += 2;
if (c == 0 || c == '/')
return -1;
}
if (n + 1 >= size)
return -1;
dst[n++] = (char)c;
}
dst[n] = '\0';
return 0;
}static int name_ok(const char *s)
{
if (*s == '\0' || *s == '.')
return 0;
for (; *s; s++)
if (!isalnum((unsigned char)*s) && *s != '_' && *s != '-' && *s != '.')
return 0;
return 1;
}
static void add_env(const char *name, const char *value)
{
size_t need = strlen(name) + strlen(value) + 2;
if (nenv >= 95 || envused + need > sizeof envpool)
return;
char *dst = envpool + envused;
snprintf(dst, need, "%s=%s", name, value);
envp[nenv++] = dst;
envused += need;
}- Errori tipici:
execcon percorso relativo dopochdir;outp[1]non chiusa nel padre; descrittori ereditati; nientewaitpid/timeout;system()con dati dell'utente; ambiente del server oProxypassati; programma che legge stdin fino a EOF; manca la riga vuota oContent-Type; nome non controllato; script senza#!o senzax.