Salta al contenuto
Note per Studenti Esercizio - GET condizionale con If-Modified-Since e cache su file (slide HTTP 0.9 e 1.0)

Esercizio - GET condizionale con If-Modified-Since e cache su file (slide HTTP 0.9 e 1.0)

In questa pagina 6

Testo (slide HTTP 0.9 e 1.0 del corso di Reti di Calcolatori, Ing. Informatica UniPD, «Esercizio 2»).

  • Effettuare una richiesta HTTP/1.0 a un server (le slide usano www.example.com, con l'indirizzo ottenuto da nslookup), includendo l'header Host.
  • Salvare la risposta in un file nel formato seguente: prima riga = data di ultima modifica, cioè il valore di Last-Modified o, in sua assenza, di Date; dalla seconda riga il contenuto della risposta (entity-body).
  • Le chiamate successive devono usare l'header If-Modified-Since per fare GET condizionali.
  • Se il server risponde con una nuova versione (200), aggiornare il file e mostrare il contenuto; se risponde 304, mostrare il contenuto presente nel file.

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 →, 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 →, Esercizio - Client HTTP 0.9 e 1.0 che scarica una pagina (slide HTTP 0.9 e 1.0).


Idea

È una cache lato client minimale. Il file di cache contiene due cose: la data che il server ha dato alla risorsa e il corpo. Ad ogni esecuzione:

 esiste il file di cache?
    no  -> GET normale
    sì  -> GET con "If-Modified-Since: <data della prima riga>"

 risposta del server:
    200 OK            -> nuova versione: riscrivi il file (data = Last-Modified oppure Date) e mostra il corpo
    304 Not Modified  -> nessun corpo: mostra il corpo salvato nel file
    altro (404, 500)  -> segnala l'errore e non toccare la cache

Due scelte progettuali:

  • La data nell'If-Modified-Since si rimanda identica alla stringa ricevuta: non occorre convertirla in numeri (si evitano strptime e fusi orari) ed è esattamente quello che il server si aspetta.
  • Se il server non manda Last-Modified si usa Date: è la data del messaggio, quindi un 304 non arriverà mai (il server non sa se la risorsa è cambiata rispetto a quel momento); la cache funzionerà ma si riscarica ogni volta. È il comportamento richiesto dal testo.

Codice

c
/* cget.c - GET condizionata HTTP/1.0 con cache su file.
 *
 * File di cache: prima riga = data dell'ultima modifica (Last-Modified, o Date se manca),
 *                dalla seconda riga = corpo della risposta.
 * Prima chiamata: GET normale, salva la cache.  Chiamate successive: If-Modified-Since.
 *   200 -> aggiorna la cache e mostra il corpo;  304 -> mostra il corpo salvato.
 *
 * Compilare:  gcc -Wall -Wextra -o cget cget.c
 * Usare:      ./cget host porta percorso file_cache
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <strings.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>

#define MAX_LINE 4096

static int connect_to(const char *host, const char *port)
{
    struct addrinfo hints, *res, *p;
    memset(&hints, 0, sizeof hints);
    hints.ai_family = AF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;
    if (getaddrinfo(host, port, &hints, &res) != 0)
        return -1;
    int fd = -1;
    for (p = res; p != NULL; p = p->ai_next) {
        fd = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
        if (fd < 0)
            continue;
        if (connect(fd, p->ai_addr, p->ai_addrlen) == 0)
            break;
        close(fd);
        fd = -1;
    }
    freeaddrinfo(res);
    return fd;
}

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;
}

static int read_line(int fd, char *line, size_t max)
{
    size_t n = 0;
    char c;
    while (n + 1 < max) {
        if (read(fd, &c, 1) <= 0)
            return -1;
        if (c == '\n') {
            if (n > 0 && line[n - 1] == '\r')
                n--;
            line[n] = '\0';
            return (int)n;
        }
        line[n++] = c;
    }
    return -1;
}

/* Se la riga e' l'header 'name', torna un puntatore al valore (senza spazi iniziali), altrimenti NULL. */
static const char *header_value(const char *line, const char *name)
{
    size_t k = strlen(name);
    if (strncasecmp(line, name, k) != 0 || line[k] != ':')
        return NULL;
    const char *v = line + k + 1;
    while (*v == ' ' || *v == '\t')
        v++;
    return v;
}

/* Carica la cache: torna 1 se esiste. date: prima riga; body/len: il resto (allocato con malloc). */
static int load_cache(const char *path, char *date, size_t dsize, char **body, size_t *len)
{
    FILE *f = fopen(path, "rb");
    if (f == NULL)
        return 0;
    if (fgets(date, (int)dsize, f) == NULL) {
        fclose(f);
        return 0;
    }
    date[strcspn(date, "\r\n")] = '\0';
    size_t cap = 4096, n = 0;
    char *b = malloc(cap);
    size_t r;
    while (b != NULL && (r = fread(b + n, 1, cap - n, f)) > 0) {
        n += r;
        if (n == cap) {
            cap *= 2;
            b = realloc(b, cap);
        }
    }
    fclose(f);
    if (b == NULL)
        return 0;
    *body = b;
    *len = n;
    return 1;
}

int main(int argc, char **argv)
{
    if (argc != 5) {
        fprintf(stderr, "uso: %s host porta percorso file_cache\n", argv[0]);
        return 1;
    }
    const char *host = argv[1], *port = argv[2], *path = argv[3], *cachefile = argv[4];

    char cdate[128] = "";
    char *cbody = NULL;
    size_t clen = 0;
    int have_cache = load_cache(cachefile, cdate, sizeof cdate, &cbody, &clen);

    int fd = connect_to(host, port);
    if (fd < 0) {
        fprintf(stderr, "connessione fallita\n");
        return 1;
    }

    /* Richiesta: se abbiamo la cache aggiungiamo If-Modified-Since con la data memorizzata, cosi' com'e'. */
    char req[2048];
    int n = snprintf(req, sizeof req, "GET %s HTTP/1.0\r\nHost: %s\r\nUser-Agent: cget/1.0\r\n", path, host);
    if (have_cache)
        n += snprintf(req + n, sizeof req - (size_t)n, "If-Modified-Since: %s\r\n", cdate);
    n += snprintf(req + n, sizeof req - (size_t)n, "\r\n");
    if (write_all(fd, req, (size_t)n) < 0) {
        perror("write");
        return 1;
    }

    /* Status-line */
    char line[MAX_LINE];
    int status = 0;
    if (read_line(fd, line, sizeof line) < 0 || sscanf(line, "HTTP/%*d.%*d %d", &status) != 1) {
        fprintf(stderr, "risposta non valida\n");
        return 1;
    }

    /* Header: ci servono solo Last-Modified e Date */
    char last_modified[128] = "", date[128] = "";
    for (;;) {
        int len = read_line(fd, line, sizeof line);
        if (len < 0) {
            fprintf(stderr, "risposta interrotta\n");
            return 1;
        }
        if (len == 0)
            break;
        const char *v;
        if ((v = header_value(line, "Last-Modified")) != NULL)
            snprintf(last_modified, sizeof last_modified, "%s", v);
        else if ((v = header_value(line, "Date")) != NULL)
            snprintf(date, sizeof date, "%s", v);
    }

    if (status == 304) {
        /* 304 Not Modified: nessun corpo, la copia locale e' ancora buona. */
        if (!have_cache) {
            fprintf(stderr, "304 ma non ho nessuna cache?\n");
            return 1;
        }
        fprintf(stderr, "[304: uso la cache, modificata il %s]\n", cdate);
        fwrite(cbody, 1, clen, stdout);
    } else if (status == 200) {
        /* Nuova versione: leggo il corpo fino alla chiusura e riscrivo la cache. */
        size_t cap = 8192, len = 0;
        char *body = malloc(cap);
        ssize_t r;
        char buf[4096];
        while (body != NULL && (r = read(fd, buf, sizeof buf)) > 0) {
            if (len + (size_t)r > cap) {
                while (len + (size_t)r > cap)
                    cap *= 2;
                body = realloc(body, cap);
                if (body == NULL)
                    break;
            }
            memcpy(body + len, buf, (size_t)r);
            len += (size_t)r;
        }
        if (body == NULL) {
            fprintf(stderr, "memoria esaurita\n");
            return 1;
        }
        const char *stamp = last_modified[0] ? last_modified : date;   /* Last-Modified, altrimenti Date */
        FILE *f = fopen(cachefile, "wb");
        if (f == NULL) {
            perror("fopen cache");
            return 1;
        }
        fprintf(f, "%s\n", stamp);
        fwrite(body, 1, len, f);
        fclose(f);
        fprintf(stderr, "[200: nuova versione (%s), %zu byte, cache aggiornata]\n", stamp, len);
        fwrite(body, 1, len, stdout);
        free(body);
    } else {
        fprintf(stderr, "[stato %d: cache non modificata]\n", status);
    }
    close(fd);
    free(cbody);
    return 0;
}

Compilare e provare

$ gcc -Wall -Wextra -o cget cget.c
$ (cd www && python3 -m http.server 8000 --bind 127.0.0.1) &

$ ./cget 127.0.0.1 8000 /index.html cache.txt
[200: nuova versione (Sun, 11 Oct 2026 20:00:25 GMT), 40 byte, cache aggiornata]
<html><body><h1>Ciao</h1></body></html>

$ ./cget 127.0.0.1 8000 /index.html cache.txt
[304: uso la cache, modificata il Sun, 11 Oct 2026 20:00:25 GMT]
<html><body><h1>Ciao</h1></body></html>

$ head -c 60 cache.txt | od -c | head -3        # prima riga = data, poi il corpo
0000000   S   u   n   ,       1   1       O   c   t       2   0   2   6
0000020       2   0   :   0   0   :   2   5       G   M   T  \n   <   h
...

$ touch www/index.html                            # il file viene modificato sul server
$ ./cget 127.0.0.1 8000 /index.html cache.txt
[200: nuova versione (Sun, 11 Oct 2026 20:03:26 GMT), 40 byte, cache aggiornata]
...

Per vedere i messaggi: curl -v --http1.0 -H 'If-Modified-Since: Sun, 11 Oct 2026 20:00:25 GMT' http://127.0.0.1:8000/index.html risponde HTTP/1.0 304 Not Modified. (Si noti: [...] e le altre diagnostiche sono su stderr, il corpo su stdout.)

Spiegazione dei punti chiave

load_cache.

  • fopen(path, "rb") fallisce se il file non esiste: è la prima esecuzione, have_cache vale 0 e la richiesta sarà non condizionale.
  • fgets(date, ...) legge la prima riga (la data) e date[strcspn(date, "\r\n")] = '\0' toglie il fine riga.
  • Il corpo si legge con fread in un buffer che si raddoppia quando è pieno (realloc): non si conosce la dimensione in anticipo. Il corpo può contenere byte zero: si conserva la lunghezza clen, non si usa strlen.

La richiesta. snprintf costruisce la riga di richiesta e gli header fissi; se c'è la cache si aggiunge If-Modified-Since: <data>\r\n; infine \r\n chiude gli header. Il server confronta la data con quella di modifica della risorsa e risponde 304 se non è cambiata.

header_value. Confronta l'inizio della riga con il nome (strncasecmp, senza distinguere maiuscole) e controlla che subito dopo ci sia :; restituisce un puntatore al valore dopo gli spazi. Serve solo per Last-Modified e Date: gli altri header si scartano.

Il ramo 304. Non c'è corpo: la risposta termina con la riga vuota. Il client stampa il corpo salvato (fwrite(cbody, 1, clen, stdout)). Se arrivasse un 304 senza che esista una cache (non potrebbe, perché non avremmo mandato If-Modified-Since) si segnala l'errore.

Il ramo 200. Si legge il corpo fino alla chiusura della connessione (HTTP/1.0) con un buffer che cresce (realloc, controllando che non fallisca). Poi si sceglie la data (Last-Modified se presente, altrimenti Date) e si riscrive il file: prima riga la data, poi il corpo con fwrite (non fputs: byte zero). Infine si stampa il corpo.

Altri codici. Un 404 o 500 non deve cancellare una cache valida: il programma stampa lo stato e lascia il file com'è.

Perché non si confrontano date. Il client non decide nulla: spedisce la data e lascia al server il confronto. Questo evita problemi di fuso e di formato (RFC 9110 ammette tre formati di data, ma la stringa viene dal server stesso).

Errori tipici

  • Convertire la data o riscriverla in un formato diverso: il server può non riconoscerla e rispondere sempre 200.
  • Dimenticare di togliere il \n letto da fgets dalla data: If-Modified-Since: Sun, ... GMT\n rompe la riga di richiesta.
  • Salvare il corpo con fputs o strlen e perdere i byte dopo uno zero; non aprire il file in modo binario ("wb", "rb").
  • Sovrascrivere la cache anche con 404 o 500.
  • Usare Date come Last-Modified pensando che dia 304: senza Last-Modified il server non può rispondere 304.
  • Aspettare un corpo dopo il 304: in HTTP un 304 non ha mai corpo, e in HTTP/1.0 la read termina comunque alla chiusura.
  • Non controllare realloc.

Varianti per esercitarsi

Versione ripasso

  • Testo. Client HTTP/1.0 con Host; file di cache: riga 1 = Last-Modified (o Date se manca), dalla riga 2 il corpo; chiamate successive con If-Modified-Since; 200 -> riscrivere il file e mostrare; 304 -> mostrare il file.
  • Schema.
file di cache esiste?  no -> GET        sì -> GET + "If-Modified-Since: <riga 1>"
200 -> salva (data, corpo), mostra       304 -> mostra il corpo salvato       altro -> errore, cache intatta
  • Data. Si rimanda identica come stringa: niente strptime, il confronto lo fa il server. Senza Last-Modified si usa Date: non darà mai 304 (comportamento voluto dal testo).
  • load_cache. fopen("rb") (non esiste = prima esecuzione); fgets per la data e date[strcspn(date, "\r\n")] = '\0'; corpo con fread e buffer raddoppiato con realloc (lunghezza clen, mai strlen: byte zero).
  • Richiesta. GET path HTTP/1.0\r\nHost: ...\r\nUser-Agent: ...\r\n + (se c'è cache) If-Modified-Since: <data>\r\n + \r\n.
  • Risposta. Status-line con sscanf("HTTP/%*d.%*d %d"); header: solo Last-Modified e Date (strncasecmp); 304 = nessun corpo; 200 = lettura fino alla chiusura (read == 0) con buffer che cresce, poi fprintf(f, "%s\n", stamp) + fwrite(body, 1, len, f), file "wb".
  • Prove. python3 -m http.server 8000; 1ª esecuzione [200: nuova versione ..., cache aggiornata]; 2ª [304: uso la cache ...]; dopo touch del file sul server di nuovo 200. Diagnostica su stderr, corpo su stdout.
  • Codice essenziale (le funzioni centrali, senza commenti):
c
static int load_cache(const char *path, char *date, size_t dsize, char **body, size_t *len)
{
    FILE *f = fopen(path, "rb");
    if (f == NULL)
        return 0;
    if (fgets(date, (int)dsize, f) == NULL) {
        fclose(f);
        return 0;
    }
    date[strcspn(date, "\r\n")] = '\0';
    size_t cap = 4096, n = 0;
    char *b = malloc(cap);
    size_t r;
    while (b != NULL && (r = fread(b + n, 1, cap - n, f)) > 0) {
        n += r;
        if (n == cap) {
            cap *= 2;
            b = realloc(b, cap);
        }
    }
    fclose(f);
    if (b == NULL)
        return 0;
    *body = b;
    *len = n;
    return 1;
}

static const char *header_value(const char *line, const char *name)
{
    size_t k = strlen(name);
    if (strncasecmp(line, name, k) != 0 || line[k] != ':')
        return NULL;
    const char *v = line + k + 1;
    while (*v == ' ' || *v == '\t')
        v++;
    return v;
}

static int connect_to(const char *host, const char *port)
{
    struct addrinfo hints, *res, *p;
    memset(&hints, 0, sizeof hints);
    hints.ai_family = AF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;
    if (getaddrinfo(host, port, &hints, &res) != 0)
        return -1;
    int fd = -1;
    for (p = res; p != NULL; p = p->ai_next) {
        fd = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
        if (fd < 0)
            continue;
        if (connect(fd, p->ai_addr, p->ai_addrlen) == 0)
            break;
        close(fd);
        fd = -1;
    }
    freeaddrinfo(res);
    return fd;
}
  • Errori tipici: data riscritta in altro formato; \n della data non tolto; fputs/strlen sul corpo e file non binario; cache sovrascritta con 404/500; Date creduta valida per 304; realloc non controllato.

Esercizi su questo argomento

Teoria collegata