Salta al contenuto
Note per Studenti Esercizio - Server HTTP con CGI (sul modello della prova pratica)

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:

  1. verificare che NOME sia un file eseguibile nella cartella cgi-bin (404 se non esiste, 403 se non è eseguibile; nomi con caratteri non sicuri e .. non ammessi);
  2. lanciare il programma con fork, due pipe, dup2 ed exec, 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_ADDR e gli header della richiesta come HTTP_*;
  3. scrivere sullo standard input del programma il corpo della POST (CONTENT_LENGTH byte);
  4. leggere tutto lo standard output del programma e interpretarlo (header CGI: Content-Type, Status, Location; riga vuota; corpo), costruendo la risposta HTTP con Content-Length;
  5. gestire gli errori: programma che fallisce o produce un'uscita non valida (502), che si blocca (timeout di 10 s), corpo troppo grande (413), POST senza Content-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-variabili

Fasi di handle:

  1. Request line e header: si memorizza ogni header (nome e valore) per costruire HTTP_*; si tengono a parte Content-Length, Content-Type, Host.
  2. Controlli: metodo GET/POST/HEAD; 411 se POST senza Content-Length o con Transfer-Encoding (un corpo CGI ha bisogno di CONTENT_LENGTH); 413 se il corpo supera 60 000 byte.
  3. Instradamento: /cgi-bin/NOME[/PATH_INFO]: il nome è fino al secondo /, il resto è PATH_INFO (che il server decodifica, mentre QUERY_STRING resta grezza); il nome ammette solo lettere, cifre, _, -, ., e non inizia con .: così non si può uscire da cgi-bin.
  4. Corpo: lettura di esattamente CONTENT_LENGTH byte con rb_readn.
  5. Ambiente: add_env costruisce envp in un'area statica.
  6. run_cgi: pipe, fork, dup2, exec (sotto).
  7. cgi_parse: interpreta l'uscita e send_response produce la risposta.

Codice

Il server

c
/* 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):

sh
#!/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:

c
/* 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:

sh
#!/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 Required

Spiegazione 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'header Host senza 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 con pct_decode: rifiuta %2F, %00 e sequenze rotte con 400), QUERY_STRING (grezza: la decodifica spetta al programma), REMOTE_ADDR;
  • CONTENT_LENGTH e CONTENT_TYPE solo se presenti;
  • per ogni altro header, HTTP_ + il nome in maiuscolo con - sostituito da _ (User-Agent → HTTP_USER_AGENT). Si escludono Authorization (le credenziali non si passano al programma), Host e, soprattutto, Proxy: l'header Proxy: ... diventerebbe HTTP_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) e outp (programma → server, diventa stdout). Ogni pipe ha una estremità di lettura [0] e una di scrittura [1].
  • dup2(inp[0], STDIN_FILENO) e dup2(outp[1], STDOUT_FILENO) collegano i descrittori standard alle pipe prima di exec: 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 ignora SIGPIPE, e una disposizione "ignora" sopravvive a exec: il programma CGI deve ricevere il comportamento normale.
  • chdir(dir): il programma gira nella propria cartella (come richiesto dalla prassi CGI); per questo dopo chdir il percorso da eseguire è "./nome", non quello relativo alla radice (un errore sottile con root_dir relativa come ./www: dopo il chdir il percorso ./www/cgi-bin/nome non esisterebbe più).
  • alarm(CGI_TIMEOUT) prima di exec: il timer sopravvive a exec e SIGALRM termina il programma dopo 10 secondi: un programma bloccato non blocca il server.
  • execle(local, name, (char *)NULL, envp): argv[0] è il nome; l'ambiente è solo envp, non quello del server. Se exec fallisce (permessi, script senza #!), il figlio esce con _exit(127).
  • Il padre chiude inp[0] e outp[1]: sono le estremità che non usa. Chiudere outp[1] è essenziale: read(outp[0]) restituisce 0 (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 legge stdin vede 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 a MAX_OUTPUT (4 MiB, poi 502); infine waitpid raccoglie il figlio (niente zombie) e WIFEXITED/WEXITSTATUS dicono se è terminato con codice 0.

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

  • exec con percorso relativo dopo chdir: il programma non si trova (errore 127/502). Si esegue ./nome nella cartella corrente.
  • Non chiudere outp[1] nel padre (il server si blocca in read) o inp[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 usa exec con argomenti separati.
  • Passare al programma l'ambiente del server (segreti) o l'header Proxy (httpoxy).
  • Il programma legge stdin fino a EOF: può non arrivare mai; deve leggere CONTENT_LENGTH byte.
  • Il programma non stampa la riga vuota dopo gli header o dimentica Content-Type: 502.
  • Nomi dei programmi non controllati (../): esecuzione di file fuori da cgi-bin.
  • Script senza #! o senza il permesso di esecuzione (ENOEXEC, EACCES).
  • Aspettarsi PATH_INFO ancora codificato, o QUERY_STRING già decodificata.

Varianti per esercitarsi

Versione ripasso

  • Testo. Server ./cgi_server porta radice che esegue radice/cgi-bin/NOME con fork, due pipe, dup2, exec; meta-variabili nell'ambiente; corpo della POST su stdin; uscita (header CGI, riga vuota, corpo) -> risposta HTTP con Content-Length; errori 404, 403, 411, 413, 502, timeout.
  • Flusso di handle. Request line + header (si tengono per HTTP_*; Content-Length, Content-Type, Host a parte) -> metodo GET/POST/HEAD (405) -> 411 se POST senza Content-Length o con Transfer-Encoding -> 413 oltre 60 000 byte -> /cgi-bin/NOME[/PATH_INFO] con nome sicuro (lettere, cifre, _ - ., non .) -> stat (404) e access(X_OK) (403) -> corpo con rb_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, %00 rifiutati con 400), QUERY_STRING (grezza), REMOTE_ADDR, CONTENT_LENGTH/CONTENT_TYPE se presenti, HTTP_NOME per gli altri header; esclusi Authorization, Host, Proxy (httpoxy).
  • run_cgi.
c
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); senza Status: 200 (o 302 se c'è Location); con un corpo serve Content-Type; non valida -> 502. send_response: status, Date, Server, header del programma, Content-Length, Connection: close, corpo (non per HEAD).
  • 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; waitpid sempre.
  • 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 -> 302 con Location assoluto; script che esce con 3 senza output o che dorme 30 s -> 502; chmod -x -> 403.
  • Codice essenziale (le funzioni centrali, senza commenti):
c
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;
}
c
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: exec con percorso relativo dopo chdir; outp[1] non chiusa nel padre; descrittori ereditati; niente waitpid/timeout; system() con dati dell'utente; ambiente del server o Proxy passati; programma che legge stdin fino a EOF; manca la riga vuota o Content-Type; nome non controllato; script senza #! o senza x.

Teoria collegata