Salta al contenuto
Note per Studenti Esercizio - Server HTTP iterativo con GET, HEAD e codici di errore (sul modello della prova pratica)

Esercizio - Server HTTP iterativo con GET, HEAD e codici di errore (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, per Linux, un server HTTP che riceve come argomenti la porta e la cartella radice dei documenti, e serve un client alla volta. Deve:

  1. rispondere a GET con il file richiesto (200 OK), con gli header Content-Type (dal tipo del file), Content-Length, Last-Modified e Date;
  2. rispondere a HEAD con gli stessi header e senza corpo;
  3. usare i codici di errore corretti: 400 richiesta malformata, 403 accesso vietato (percorsi che escono dalla radice), 404 file inesistente, 405 metodo non permesso (con l'header Allow), 414, 431, 501 metodo sconosciuto, 505 versione non supportata, e 301 per una cartella senza / finale;
  4. impedire l'accesso a file fuori dalla radice (path traversal);
  5. accettare anche una richiesta nel vecchio formato HTTP/0.9 (GET /percorso), rispondendo con il solo documento.

Teoria: 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 →, Client e server TCP in C - bind, listen, accept e processi concorrentiUn client TCP in C fa getaddrinfo, socket, connect, poi write e read; un server fa socket, setsockopt(SO_REUSEADDR), bind, listen e un ciclo di accept. Un server iterativo serve un client alla volta e un client lento blocca tutti; un server concorrente con fork crea un processo figlio per ogni connessione: il figlio chiude il socket di ascolto e serve la connessione, il padre chiude il socket di connessione e torna ad accept. I figli terminati diventano zombie finche' il padre non li raccoglie con waitpid, di solito in un gestore di SIGCHLD con WNOHANG in un ciclo; i descrittori non chiusi tengono viva la connessione (nessun FIN) o esauriscono EMFILE; uno stato CLOSE_WAIT che si accumula indica una close mancante. Alternative: thread, pre-fork, I/O multiplexing con poll o epoll.Client e server TCP in C - bind, listen, accept e processi concorrenti →, 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 →, 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 →.


Idea

Il server ha lo scheletro iterativo di Client e server TCP in C - bind, listen, accept e processi concorrentiUn client TCP in C fa getaddrinfo, socket, connect, poi write e read; un server fa socket, setsockopt(SO_REUSEADDR), bind, listen e un ciclo di accept. Un server iterativo serve un client alla volta e un client lento blocca tutti; un server concorrente con fork crea un processo figlio per ogni connessione: il figlio chiude il socket di ascolto e serve la connessione, il padre chiude il socket di connessione e torna ad accept. I figli terminati diventano zombie finche' il padre non li raccoglie con waitpid, di solito in un gestore di SIGCHLD con WNOHANG in un ciclo; i descrittori non chiusi tengono viva la connessione (nessun FIN) o esauriscono EMFILE; uno stato CLOSE_WAIT che si accumula indica una close mancante. Alternative: thread, pre-fork, I/O multiplexing con poll o epoll.Client e server TCP in C - bind, listen, accept e processi concorrenti →: socket, SO_REUSEADDR, bind, listen, ciclo di accept con handle_client(cfd) e close. La parte nuova è handle_client, che per ogni connessione fa:

  1. Legge la request line e gli header con un lettore a buffer (struct rbuf e rb_gets, che si comporta come fgets ma sul socket: legge a blocchi da 4 KiB con una read e consegna una riga alla volta).
  2. Controlla metodo, versione e target, nell'ordine che dà il codice più preciso.
  3. Trasforma il target in un file: toglie la query, decodifica il percent-encoding, rifiuta .., aggiunge index.html se finisce con /, controlla con stat se è una cartella.
  4. Apre il file (open, fstat) e invia gli header seguiti dal contenuto.
  5. Scrive una riga di log sullo standard error e chiude la connessione (Connection: close).

Il formato di ogni risposta d'errore è lo stesso: send_error genera una piccola pagina HTML e l'header Content-Length giusto, con Connection: close.

Codice

c
/* http_server.c - server HTTP iterativo: serve file statici con GET e HEAD, risponde con i codici di errore giusti.
 *
 * Compilare:  gcc -Wall -Wextra -o http_server http_server.c
 * Avviare:    ./http_server 8080 ./www
 * Provare:    curl -i http://127.0.0.1:8080/index.html        (200)
 *             curl -i http://127.0.0.1:8080/nonesiste         (404)
 *             curl -i -X POST http://127.0.0.1:8080/          (405 con Allow)
 *             curl -i --path-as-is http://127.0.0.1:8080/../etc/passwd   (403)
 *             printf 'GET /index.html\r\n' | nc 127.0.0.1 8080            (HTTP/0.9: solo il corpo)
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.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/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define MAX_LINE 2048

static const char *root_dir;                       /* radice dei documenti */

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;                             /* EPIPE: il client ha chiuso, si lascia perdere */
        }
        buf += w;
        n -= (size_t)w;
    }
    return 0;
}

/* Lettore con buffer: legge dal socket a blocchi da 4 KiB e restituisce una riga alla volta, come fgets. */
struct rbuf {
    int fd;
    char buf[4096];
    size_t pos, len;                               /* byte gia' consumati / byte validi nel buffer */
};

/* Legge fino a LF compreso (al massimo max-1 caratteri). NULL se la connessione e' finita senza dati. */
static char *rb_gets(struct rbuf *r, char *line, size_t max)
{
    size_t n = 0;
    while (n + 1 < max) {
        if (r->pos == r->len) {                    /* buffer vuoto: si riempie con una read */
            ssize_t k = read(r->fd, r->buf, sizeof r->buf);
            if (k < 0 && errno == EINTR)
                continue;
            if (k <= 0)
                break;                             /* EOF o errore */
            r->pos = 0;
            r->len = (size_t)k;
        }
        char c = r->buf[r->pos++];
        line[n++] = c;
        if (c == '\n')
            break;
    }
    line[n] = '\0';
    return n > 0 ? line : NULL;
}

/* Tipo MIME dall'estensione del file. */
static const char *mime_type(const char *path)
{
    static const struct { const char *ext, *type; } tab[] = {
        {".html", "text/html; charset=utf-8"}, {".htm", "text/html; charset=utf-8"},
        {".css", "text/css"}, {".js", "text/javascript"}, {".json", "application/json"},
        {".txt", "text/plain; charset=utf-8"}, {".png", "image/png"}, {".jpg", "image/jpeg"},
        {".jpeg", "image/jpeg"}, {".gif", "image/gif"}, {".svg", "image/svg+xml"},
        {".pdf", "application/pdf"}, {".ico", "image/x-icon"},
    };
    const char *dot = strrchr(path, '.');
    if (dot != NULL)
        for (size_t i = 0; i < sizeof tab / sizeof tab[0]; i++)
            if (strcasecmp(dot, tab[i].ext) == 0)
                return tab[i].type;
    return "application/octet-stream";             /* tipo sconosciuto: il browser scarica e basta */
}

static void http_date(time_t t, char *out, size_t size)
{
    struct tm *g = gmtime(&t);
    strftime(out, size, "%a, %d %b %Y %H:%M:%S GMT", g);   /* formato IMF-fixdate, sempre GMT */
}

/* Risposta di errore con un piccolo corpo HTML. extra: header aggiuntivi (con CRLF) oppure "". */
static void send_error(int fd, int code, const char *reason, const char *extra, int head_only)
{
    char body[512], head[1024], date[64];
    int blen = snprintf(body, sizeof body,
                        "<html><head><title>%d %s</title></head><body><h1>%d %s</h1></body></html>\n",
                        code, reason, code, reason);
    http_date(time(NULL), date, sizeof date);
    int hlen = snprintf(head, sizeof head,
                        "HTTP/1.1 %d %s\r\n"
                        "Date: %s\r\n"
                        "Server: mini-http/1.0\r\n"
                        "Content-Type: text/html; charset=utf-8\r\n"
                        "Content-Length: %d\r\n"
                        "Connection: close\r\n"
                        "%s"
                        "\r\n",
                        code, reason, date, blen, extra);
    write_all(fd, head, (size_t)hlen);
    if (!head_only)
        write_all(fd, body, (size_t)blen);
}

static int hexval(int c)
{
    if (c >= '0' && c <= '9') return c - '0';
    if (c >= 'a' && c <= 'f') return c - 'a' + 10;
    if (c >= 'A' && c <= 'F') return c - 'A' + 10;
    return -1;
}

/* Decodifica %XX. Torna -1 se la sequenza e' rotta o produce un byte NUL. */
static int url_decode(const char *src, char *dst, size_t dsize)
{
    size_t n = 0;
    for (; *src; src++) {
        int c = (unsigned char)*src;
        if (c == '%') {
            int h = hexval((unsigned char)src[1]);
            int l = (h >= 0) ? hexval((unsigned char)src[2]) : -1;
            if (h < 0 || l < 0)
                return -1;
            c = h * 16 + l;
            src += 2;
            if (c == 0)
                return -1;
        }
        if (n + 1 >= dsize)
            return -1;
        dst[n++] = (char)c;
    }
    dst[n] = '\0';
    return 0;
}

/* Vero se nessun segmento del percorso e' "..": impedisce di uscire dalla radice (path traversal). */
static int path_is_safe(const char *p)
{
    for (const char *s = p; (s = strstr(s, "..")) != NULL; s += 2) {
        int start = (s == p) || s[-1] == '/';
        int end = (s[2] == '\0') || s[2] == '/';
        if (start && end)
            return 0;
    }
    return 1;
}

/* Manda al client il file aperto; per HEAD solo gli header. */
static void send_file(int fd, int filefd, const char *path, const struct stat *st, int head_only)
{
    char head[1024], date[64], lm[64];
    http_date(time(NULL), date, sizeof date);
    http_date(st->st_mtime, lm, sizeof lm);
    int hlen = snprintf(head, sizeof head,
                        "HTTP/1.1 200 OK\r\n"
                        "Date: %s\r\n"
                        "Server: mini-http/1.0\r\n"
                        "Last-Modified: %s\r\n"
                        "Content-Type: %s\r\n"
                        "Content-Length: %lld\r\n"
                        "Connection: close\r\n"
                        "\r\n",
                        date, lm, mime_type(path), (long long)st->st_size);
    if (write_all(fd, head, (size_t)hlen) < 0 || head_only)
        return;
    char buf[8192];
    ssize_t r;
    while ((r = read(filefd, buf, sizeof buf)) > 0)
        if (write_all(fd, buf, (size_t)r) < 0)
            break;
}

static void handle_client(int fd, const char *peer)
{
    struct rbuf in = {.fd = fd};                   /* lettura a righe; si scrive con write(fd) */
    char line[MAX_LINE], method[16], target[MAX_LINE], version[16];

    /* ---- request line ---- */
    if (rb_gets(&in, line, sizeof line) == NULL) {
        close(fd);                                 /* il client ha chiuso senza dire nulla */
        return;
    }
    if (strchr(line, '\n') == NULL) {              /* riga piu' lunga del buffer */
        send_error(fd, 414, "URI Too Long", "", 0);
        close(fd);
        return;
    }
    line[strcspn(line, "\r\n")] = '\0';
    int ntok = sscanf(line, "%15s %2047s %15s", method, target, version);
    int http09 = (ntok == 2);                      /* "GET /pagina" senza versione: HTTP/0.9 */
    if (ntok < 2) {
        send_error(fd, 400, "Bad Request", "", 0);
        close(fd);
        return;
    }

    /* ---- header: si leggono e si scartano fino alla riga vuota (qui non servono) ---- */
    if (!http09) {
        int nh = 0;
        while (rb_gets(&in, line, sizeof line) != NULL && strcmp(line, "\r\n") != 0 && strcmp(line, "\n") != 0)
            if (++nh > 100) {
                send_error(fd, 431, "Request Header Fields Too Large", "", 0);
                close(fd);
                return;
            }
    }

    int status = 200;
    int head_only = (strcmp(method, "HEAD") == 0);

    if (!http09 && strncmp(version, "HTTP/1.", 7) != 0) {
        status = 505;
        send_error(fd, 505, "HTTP Version Not Supported", "", head_only);
    } else if (strcmp(method, "GET") != 0 && !head_only) {
        /* metodo noto ma non permesso: 405 + Allow; metodo sconosciuto: 501 */
        static const char *known[] = {"POST", "PUT", "DELETE", "OPTIONS", "TRACE", "CONNECT", "PATCH"};
        int is_known = 0;
        for (size_t i = 0; i < sizeof known / sizeof known[0]; i++)
            if (strcmp(method, known[i]) == 0)
                is_known = 1;
        if (is_known) {
            status = 405;
            send_error(fd, 405, "Method Not Allowed", "Allow: GET, HEAD\r\n", 0);
        } else {
            status = 501;
            send_error(fd, 501, "Not Implemented", "", 0);
        }
    } else if (target[0] != '/') {
        status = 400;
        send_error(fd, 400, "Bad Request", "", head_only);
    } else {
        /* ---- dal target al file ---- */
        char *q = strchr(target, '?');
        if (q != NULL)
            *q = '\0';                             /* la query non serve per un file statico */
        char path[MAX_LINE];
        if (url_decode(target, path, sizeof path) < 0) {
            status = 400;
            send_error(fd, 400, "Bad Request", "", head_only);
        } else if (!path_is_safe(path)) {
            status = 403;
            send_error(fd, 403, "Forbidden", "", head_only);
        } else {
            char full[MAX_LINE * 2];
            size_t plen = strlen(path);
            snprintf(full, sizeof full, "%s%s%s", root_dir, path, path[plen - 1] == '/' ? "index.html" : "");
            struct stat st;
            if (stat(full, &st) == 0 && S_ISDIR(st.st_mode)) {
                /* "/docs" senza barra finale: redirect a "/docs/" */
                char extra[MAX_LINE + 32];
                snprintf(extra, sizeof extra, "Location: %s/\r\n", path);
                status = 301;
                send_error(fd, 301, "Moved Permanently", extra, head_only);
            } else {
                int ffd = open(full, O_RDONLY);
                if (ffd < 0 || fstat(ffd, &st) < 0 || !S_ISREG(st.st_mode)) {
                    status = (ffd >= 0 || errno != EACCES) ? 404 : 403;
                    if (status == 404)
                        send_error(fd, 404, "Not Found", "", head_only);
                    else
                        send_error(fd, 403, "Forbidden", "", head_only);
                } else if (http09) {
                    char buf[8192];                /* HTTP/0.9: solo il documento, niente header */
                    ssize_t r;
                    while ((r = read(ffd, buf, sizeof buf)) > 0)
                        if (write_all(fd, buf, (size_t)r) < 0)
                            break;
                } else {
                    send_file(fd, ffd, full, &st, head_only);
                }
                if (ffd >= 0)
                    close(ffd);
            }
        }
    }
    fprintf(stderr, "%s \"%s %s\" -> %d\n", peer, method, target, status);
    close(fd);                                     /* Connection: close */
}

int main(int argc, char **argv)
{
    if (argc != 3) {
        fprintf(stderr, "uso: %s porta radice\n", argv[0]);
        return 1;
    }
    int port = atoi(argv[1]);
    root_dir = argv[2];
    signal(SIGPIPE, SIG_IGN);                      /* scrivere su un socket chiuso non deve uccidere il server */

    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);   /* riavvio immediato anche con TIME_WAIT */

    struct sockaddr_in addr;
    memset(&addr, 0, sizeof addr);
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = htonl(INADDR_ANY);      /* tutte le interfacce */
    addr.sin_port = htons((unsigned short)port);   /* la porta va in network byte order */
    if (bind(lfd, (struct sockaddr *)&addr, sizeof addr) < 0) {
        perror("bind");
        return 1;
    }
    if (listen(lfd, 16) < 0) {
        perror("listen");
        return 1;
    }
    fprintf(stderr, "in ascolto sulla porta %d, radice %s\n", port, root_dir);

    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 + 8];
        inet_ntop(AF_INET, &cli.sin_addr, peer, sizeof peer);
        handle_client(cfd, peer);                  /* iterativo: un client alla volta */
    }
    close(lfd);
    return 0;
}

Compilare e provare

$ mkdir -p www/docs && echo '<html><body><h1>Ciao</h1></body></html>' > www/index.html && echo '<p>docs</p>' > www/docs/index.html
$ gcc -Wall -Wextra -o http_server http_server.c
$ ./http_server 8080 ./www &
in ascolto sulla porta 8080, radice ./www

Con curl -i (stampa anche gli header di risposta):

$ curl -i http://127.0.0.1:8080/index.html
HTTP/1.1 200 OK
Date: Sun, 11 Oct 2026 20:21:45 GMT
Server: mini-http/1.0
Last-Modified: Sun, 11 Oct 2026 20:03:26 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 40
Connection: close

<html><body><h1>Ciao</h1></body></html>

$ curl -I http://127.0.0.1:8080/index.html          # HEAD: stessi header, nessun corpo
$ curl -i http://127.0.0.1:8080/nonesiste
HTTP/1.1 404 Not Found
...Content-Length: 90...
<html><head><title>404 Not Found</title></head><body><h1>404 Not Found</h1></body></html>

$ curl -i http://127.0.0.1:8080/docs                # cartella senza barra finale
HTTP/1.1 301 Moved Permanently
Location: /docs/

$ curl -i http://127.0.0.1:8080/docs/               # diventa docs/index.html
HTTP/1.1 200 OK ... <p>docs</p>

$ curl -i -X POST http://127.0.0.1:8080/
HTTP/1.1 405 Method Not Allowed
Allow: GET, HEAD

$ curl -i -X FOO http://127.0.0.1:8080/             # metodo sconosciuto
HTTP/1.1 501 Not Implemented

$ curl -i --path-as-is http://127.0.0.1:8080/../etc/passwd      # path traversal
HTTP/1.1 403 Forbidden
$ curl -i --path-as-is http://127.0.0.1:8080/%2e%2e/etc/passwd  # lo stesso, codificato
HTTP/1.1 403 Forbidden

(--path-as-is impedisce a curl di normalizzare il percorso da solo.) A mano con nc, per vedere cosa succede con richieste scorrette o con HTTP/0.9:

$ printf 'GET /index.html\r\n' | nc 127.0.0.1 8080            # HTTP/0.9: solo il documento, nessun header
<html><body><h1>Ciao</h1></body></html>
$ printf 'GET / HTTP/2.0\r\n\r\n' | nc 127.0.0.1 8080 | head -1
HTTP/1.1 505 HTTP Version Not Supported
$ printf 'garbage\r\n\r\n' | nc 127.0.0.1 8080 | head -1
HTTP/1.1 400 Bad Request

Il server stampa una riga per richiesta: 127.0.0.1 "GET /index.html" -> 200. Con il client dell'esercizio Esercizio - Client HTTP 0.9 e 1.0 che scarica una pagina (slide HTTP 0.9 e 1.0) (./client10 127.0.0.1 8080 /index.html) si prova lo stesso server da C.

Spiegazione dei punti chiave

rb_gets. Il buffer interno (buf, con pos byte consumati e len byte validi) si riempie con una read da 4096 byte quando è vuoto e i byte si consegnano uno alla volta fino al \n. Così il costo è una system call ogni 4 KiB, non una per byte come nelle slide, e il buffer può già contenere l'inizio dell'header successivo (o del corpo): per questo il lettore va usato per tutta la connessione, non si può alternare con read diretta.

La request line.

  • sscanf(line, "%15s %2047s %15s", method, target, version): i limiti nella stringa di formato (%15s) impediscono di scrivere oltre i buffer. Restituisce il numero di campi letti.
  • 3 campi: richiesta HTTP/1.x. 2 campi: GET /percorso senza versione, cioè il formato HTTP/0.9 (http09): si risponde con il solo contenuto, senza header. Meno di 2: 400.
  • Una riga più lunga di MAX_LINE (strchr(line, '\n') == NULL) dà 414 URI Too Long.
  • Gli header si leggono e si scartano finché rb_gets restituisce la riga vuota ("\r\n") con un tetto di 100 righe (431). Qui non servono: non c'è corpo da leggere per GET e HEAD.

Ordine dei controlli.

  1. versione: se non inizia con HTTP/1. → 505;
  2. metodo diverso da GET e HEAD: se è uno dei metodi che esistono ma questo server non fa (POST, PUT, DELETE...) → 405 Method Not Allowed con Allow: GET, HEAD; se è sconosciuto → 501 Not Implemented;
  3. target che non inizia per / → 400;
  4. percorso: decodifica e 403 per ...

url_decode e path_is_safe (come in Esercizio - Parsing di request line, header e URI (sul modello della prova pratica)). Prima si decodifica, poi si controlla; il server rifiuta anche %00. La query (?x=1) si toglie (*q = '\0') perché un file statico non la usa.

Dal percorso al file.

  • snprintf(full, ..., "%s%s%s", root_dir, path, path[plen - 1] == '/' ? "index.html" : ""): radice + percorso, e se il percorso finisce per / si aggiunge index.html.
  • stat e S_ISDIR: se è una cartella senza / finale si risponde 301 con Location: /docs/; il browser rifà la richiesta.
  • open(full, O_RDONLY): se fallisce con EACCES → 403, altrimenti 404. fstat sul descrittore aperto e S_ISREG confermano che è un file normale (non una cartella o un dispositivo). Si usa fstat dopo open (non stat prima) per evitare che il file cambi fra il controllo e l'uso.

La risposta 200 (send_file): Content-Length è la dimensione del file (st_size); Content-Type viene da mime_type, una tabella estensione→tipo con application/octet-stream come ripiego; Last-Modified è la data di modifica del file e Date l'ora attuale, entrambe in formato Sun, 11 Oct 2026 20:21:45 GMT ottenuto con gmtime e strftime (sempre GMT). Per HEAD (head_only) si inviano gli header e basta. Il corpo si invia in blocchi da 8 KiB con write_all (ciclo che gestisce le scritture parziali); EPIPE (client sparito) interrompe il ciclo senza far morire il server (signal(SIGPIPE, SIG_IGN)).

send_error. Costruisce il corpo (<h1>404 Not Found</h1>) prima, calcola la lunghezza con snprintf e la mette in Content-Length; per HEAD non invia il corpo ma l'header resta lo stesso. L'argomento extra aggiunge header supplementari (Allow, Location) già terminati da \r\n.

Il formato HTTP/0.9. Dopo GET /percorso il server invia solo i byte del file: nessuna status-line né header, e chiude. Il client capisce la fine dalla chiusura.

Connection: close. Ogni risposta annuncia la chiusura, e close(fd) la esegue: server e client sanno che c'è una richiesta per connessione. Le connessioni persistenti sono in Esercizio - Server HTTP concorrente con fork e connessioni persistenti (sul modello della prova pratica).

Limiti di questa versione. È iterativo: un client lento tiene bloccato tutto, e non c'è timeout di lettura. Non gestisce corpi in ingresso, non invia ETag né 304 (Esercizio - Server con autenticazione Basic, ETag e GET condizionale (sul modello della prova pratica)), non supporta IPv6.

Errori tipici

  • Dimenticare Content-Length o sbagliarlo (ad esempio con strlen su un file binario): il client attende per sempre o tronca.
  • Non azzerare o non controllare la query e il percorso: /index.html?x=1 non si trova come file.
  • Controllare .. prima del percent-decoding, e quindi accettare %2e%2e.
  • Costruire il percorso con sprintf senza limiti: buffer overflow con un target lungo.
  • Rispondere 404 per un metodo non permesso (è 405 con Allow) o 400 per un metodo sconosciuto (è 501).
  • Per HEAD inviare il corpo: il client lo prenderebbe come l'inizio della risposta successiva su una connessione persistente.
  • Mandare la data in ora locale invece che in GMT, o con nomi di giorno e mese localizzati (strftime usa la lingua della locale: senza setlocale resta il formato standard).
  • Dimenticare SO_REUSEADDR o signal(SIGPIPE, SIG_IGN).
  • Non chiudere il file aperto (close(ffd)): i descrittori si esauriscono.
  • Servire i file senza il controllo S_ISREG: una richiesta per una cartella o per un file speciale fa leggere dati inattesi.

Varianti per esercitarsi

Versione ripasso

  • Testo. Server ./http_server porta radice iterativo: GET -> 200 con Content-Type, Content-Length, Last-Modified, Date; HEAD senza corpo; 400, 403 (path traversal), 404, 405 + Allow, 414, 431, 501, 505, 301 (cartella senza /); anche HTTP/0.9.
  • Scheletro. socket, SO_REUSEADDR, bind (htons, INADDR_ANY), listen, for (;;) { accept; handle_client; close }; signal(SIGPIPE, SIG_IGN).
  • Lettura. rb_gets: buffer da 4 KiB (una read ogni 4 KiB), riga alla volta; si usa per tutta la connessione. Request line: sscanf("%15s %2047s %15s"): 3 campi = HTTP/1.x, 2 campi = HTTP/0.9 (solo documento), meno = 400; riga troppo lunga = 414; header scartati fino alla riga vuota, massimo 100 (431).
  • Ordine dei controlli. Versione non HTTP/1. -> 505; metodo noto ma non permesso (POST, PUT, DELETE...) -> 405 + Allow: GET, HEAD; sconosciuto -> 501; target senza / iniziale -> 400; decodifica e .. -> 403.
  • Dal target al file. Togliere la query (*q = '\0'); url_decode poi path_is_safe; radice + percorso (+ "index.html" se finisce per '/'); stat + S_ISDIR -> 301 con Location: /docs/; open (EACCES -> 403, altrimenti 404), fstat + S_ISREG.
  • Risposta 200. Content-Length = st_size, Content-Type da tabella estensione -> tipo (ripiego application/octet-stream), Last-Modified e Date in GMT (gmtime + strftime("%a, %d %b %Y %H:%M:%S GMT")), Connection: close; HEAD = solo header; corpo a blocchi con write_all. send_error: corpo HTML generato, Content-Length calcolato, extra per Allow/Location.
  • Prove. curl -i, curl -I, curl -X POST, curl -X FOO, curl --path-as-is http://127.0.0.1:8080/../etc/passwd e /%2e%2e/etc/passwd -> 403; printf 'GET /index.html\r\n' | nc 127.0.0.1 8080 -> solo il documento.
  • Limiti. Iterativo (un client lento blocca), nessun timeout, niente 304/ETag, niente corpi in ingresso.
  • Codice essenziale (le funzioni centrali, senza commenti):
c
static char *rb_gets(struct rbuf *r, char *line, size_t max)
{
    size_t n = 0;
    while (n + 1 < max) {
        if (r->pos == r->len) {
            ssize_t k = read(r->fd, r->buf, sizeof r->buf);
            if (k < 0 && errno == EINTR)
                continue;
            if (k <= 0)
                break;
            r->pos = 0;
            r->len = (size_t)k;
        }
        char c = r->buf[r->pos++];
        line[n++] = c;
        if (c == '\n')
            break;
    }
    line[n] = '\0';
    return n > 0 ? line : NULL;
}

static void send_error(int fd, int code, const char *reason, const char *extra, int head_only)
{
    char body[512], head[1024], date[64];
    int blen = snprintf(body, sizeof body,
                        "<html><head><title>%d %s</title></head><body><h1>%d %s</h1></body></html>\n",
                        code, reason, code, reason);
    http_date(time(NULL), date, sizeof date);
    int hlen = snprintf(head, sizeof head,
                        "HTTP/1.1 %d %s\r\n"
                        "Date: %s\r\n"
                        "Server: mini-http/1.0\r\n"
                        "Content-Type: text/html; charset=utf-8\r\n"
                        "Content-Length: %d\r\n"
                        "Connection: close\r\n"
                        "%s"
                        "\r\n",
                        code, reason, date, blen, extra);
    write_all(fd, head, (size_t)hlen);
    if (!head_only)
        write_all(fd, body, (size_t)blen);
}

static void send_file(int fd, int filefd, const char *path, const struct stat *st, int head_only)
{
    char head[1024], date[64], lm[64];
    http_date(time(NULL), date, sizeof date);
    http_date(st->st_mtime, lm, sizeof lm);
    int hlen = snprintf(head, sizeof head,
                        "HTTP/1.1 200 OK\r\n"
                        "Date: %s\r\n"
                        "Server: mini-http/1.0\r\n"
                        "Last-Modified: %s\r\n"
                        "Content-Type: %s\r\n"
                        "Content-Length: %lld\r\n"
                        "Connection: close\r\n"
                        "\r\n",
                        date, lm, mime_type(path), (long long)st->st_size);
    if (write_all(fd, head, (size_t)hlen) < 0 || head_only)
        return;
    char buf[8192];
    ssize_t r;
    while ((r = read(filefd, buf, sizeof buf)) > 0)
        if (write_all(fd, buf, (size_t)r) < 0)
            break;
}

static int path_is_safe(const char *p)
{
    for (const char *s = p; (s = strstr(s, "..")) != NULL; s += 2) {
        int start = (s == p) || s[-1] == '/';
        int end = (s[2] == '\0') || s[2] == '/';
        if (start && end)
            return 0;
    }
    return 1;
}
  • Errori tipici: Content-Length assente o sbagliato; percorso controllato prima della decodifica; sprintf senza limiti; 404 al posto di 405, 400 al posto di 501; corpo inviato per HEAD; data in ora locale; niente SO_REUSEADDR/SIGPIPE; file non chiuso; manca S_ISREG.

Esercizi su questo argomento

Teoria collegata