Salta al contenuto
Note per Studenti Esercizio - Server con autenticazione Basic, ETag e GET condizionale (sul modello della prova pratica)

Esercizio - Server con autenticazione Basic, ETag e GET condizionale (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 con due aree, radice/public/ e radice/private/, che riceve porta, radice e credenziali (utente:password) dalla riga di comando:

  1. GET /public/NOME: file liberi, con Cache-Control: public, max-age=60;
  2. GET /private/NOME: richiedono autenticazione Basic; senza credenziali giuste il server risponde 401 Unauthorized con WWW-Authenticate: Basic realm="Area riservata"; con quelle giuste serve il file con Cache-Control: private, no-cache;
  3. per tutti i file: header ETag (identificatore della versione) e supporto a If-None-Match: se l'ETag del client coincide con quello corrente, 304 Not Modified senza corpo, con ETag e Cache-Control;
  4. decodificare Base64 (Authorization: Basic ...) e confrontare le credenziali in tempo costante;
  5. permettere solo nomi di file semplici (niente / e niente ..): accesso fuori dalle cartelle impossibile.

Teoria: 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 →, 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 →, 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 →, WebSocket, QUIC e HTTP-3HTTP e' richiesta e risposta, quindi poco adatto a notifiche e dati in tempo reale (polling, long polling, SSE); WebSocket (RFC 6455) apre con un handshake HTTP/1.1 Upgrade (Sec-WebSocket-Key a 16 byte casuali in Base64, risposta 101 con Sec-WebSocket-Accept = Base64 di SHA-1 di chiave piu' GUID fisso) un canale bidirezionale persistente sulla stessa connessione TCP, con frame di 2-14 byte di intestazione (FIN, opcode, MASK, lunghezza su 7, 16 o 64 bit, chiave di mascheramento obbligatoria dal client al server) e messaggi di testo, binari, close, ping e pong. QUIC (RFC 9000) e' un protocollo di trasporto sopra UDP con TLS 1.3 integrato, flussi indipendenti (niente head-of-line blocking fra flussi), connection ID che permettono di cambiare rete, handshake in 1 RTT e ripresa in 0 RTT; HTTP/3 (RFC 9114) mappa HTTP su QUIC con un flusso per richiesta e QPACK per gli header, ed e' annunciato con Alt-Svc.WebSocket, QUIC e HTTP-3 → (per Base64).


Idea

Due meccanismi di HTTP in un solo server:

Autenticazione (RFC 9110 par. 11, RFC 7617).

 client                                        server
   | GET /private/riservato.txt                  |
   |-------------------------------------------->|
   |<-- 401 Unauthorized                          |
   |    WWW-Authenticate: Basic realm="Area riservata", charset="UTF-8"
   | GET /private/riservato.txt                   |
   | Authorization: Basic bWFyaW86c2VncmV0bw==    |      (base64 di "mario:segreto")
   |-------------------------------------------->|
   |<-- 200 OK + ETag + corpo                     |

Basic non cifra: base64("mario:segreto") si inverte in un attimo; si usa solo con TLS.

Cache e richieste condizionali. Ogni risposta 200 porta un ETag che identifica la versione del file; qui è "<mtime in esadecimale>-<dimensione in esadecimale>", che cambia se il file viene modificato. Il client lo rimanda in If-None-Match: se corrisponde, il corpo non si invia (304). Cache-Control dice per quanto tempo il client può riusare la copia senza chiedere (max-age=60) o se deve rivalidare sempre (no-cache).

Struttura del codice: handle legge request line e header (memorizzando Authorization e If-None-Match), controlla il metodo, il percorso e (per /private/) le credenziali, e poi risponde 304 o 200.

Codice

c
/* auth_server.c - server HTTP con autenticazione Basic, ETag e GET condizionale (304).
 *   /public/NOME   : file liberi, con Cache-Control: public, max-age=60
 *   /private/NOME  : richiedono "Authorization: Basic base64(utente:password)", altrimenti 401
 *   Per entrambi: ETag; se il client manda If-None-Match con l'ETag corrente si risponde 304 senza corpo.
 *
 * Compilare:  gcc -Wall -Wextra -o auth_server auth_server.c
 * Avviare:    ./auth_server 8080 ./www mario:segreto       (file in ./www/public e ./www/private)
 * Provare:    curl -i http://127.0.0.1:8080/private/riservato.txt                  (401 + WWW-Authenticate)
 *             curl -i -u mario:segreto http://127.0.0.1:8080/private/riservato.txt  (200 + ETag)
 *             curl -i -u mario:segreto -H 'If-None-Match: "<ETag ricevuto>"' http://127.0.0.1:8080/private/riservato.txt  (304)
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <stdint.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/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define MAX_LINE 2048

static const char *root_dir;
static const char *credentials;                    /* "utente:password" in chiaro (solo per l'esercizio) */

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

struct rbuf {
    int fd;
    char buf[4096];
    size_t pos, len;
};

static int rb_line(struct rbuf *r, char *line, size_t max)
{
    size_t n = 0;
    for (;;) {
        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)
                return -1;
            r->pos = 0;
            r->len = (size_t)k;
        }
        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;
    }
}

/* ---------- Base64 (RFC 4648): decodifica ---------- */
/* Torna la lunghezza decodificata, -1 se l'input non e' Base64 valido. */
static int base64_decode(const char *in, uint8_t *out, size_t cap)
{
    static const char T[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
    size_t n = strlen(in), o = 0;
    if (n == 0 || n % 4 != 0)
        return -1;                                 /* la lunghezza e' sempre multipla di 4 (con '=' di riempimento) */
    for (size_t i = 0; i < n; i += 4) {
        int v[4], pad = 0;
        for (int k = 0; k < 4; k++) {
            char c = in[i + (size_t)k];
            if (c == '=' && i + 4 == n && k >= 2) {   /* '=' ammesso solo nelle ultime due posizioni */
                v[k] = 0;
                pad++;
            } else {
                const char *p = (c == '\0' || pad > 0) ? NULL : strchr(T, c);
                if (p == NULL)
                    return -1;
                v[k] = (int)(p - T);
            }
        }
        uint32_t x = (uint32_t)v[0] << 18 | (uint32_t)v[1] << 12 | (uint32_t)v[2] << 6 | (uint32_t)v[3];
        if (o + 3 > cap)
            return -1;
        out[o++] = (uint8_t)(x >> 16);
        if (pad < 2)
            out[o++] = (uint8_t)(x >> 8);
        if (pad < 1)
            out[o++] = (uint8_t)x;
    }
    return (int)o;
}

/* Confronto a tempo costante: non rivela con il tempo di risposta in che posizione differiscono le stringhe. */
static int secure_equal(const char *a, const char *b)
{
    size_t la = strlen(a), lb = strlen(b);
    unsigned diff = (unsigned)(la ^ lb);
    for (size_t i = 0; i < la && i < lb; i++)
        diff |= (unsigned)(a[i] ^ b[i]);
    return diff == 0;
}

/* Vero se il valore di Authorization e' "Basic <base64 di utente:password>" corretto. */
static int check_basic(const char *auth)
{
    if (auth == NULL || strncasecmp(auth, "Basic ", 6) != 0)   /* lo schema non distingue maiuscole */
        return 0;
    const char *b64 = auth + 6;
    while (*b64 == ' ')
        b64++;
    uint8_t dec[256];
    int n = base64_decode(b64, dec, sizeof dec - 1);
    if (n < 0)
        return 0;
    dec[n] = '\0';
    return secure_equal((const char *)dec, credentials);
}

/* ---------- risposte ---------- */
static const char *mime_type(const char *name)
{
    static const struct { const char *ext, *type; } tab[] = {
        {".html", "text/html; charset=utf-8"}, {".txt", "text/plain; charset=utf-8"}, {".css", "text/css"},
        {".json", "application/json"}, {".png", "image/png"}, {".jpg", "image/jpeg"}, {".pdf", "application/pdf"},
    };
    const char *dot = strrchr(name, '.');
    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";
}

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

/* Risposta semplice con corpo di testo. extra: header supplementari con CRLF. */
static void reply(int fd, int code, const char *reason, const char *extra, const char *text, int head_only)
{
    char head[1024], date[64];
    http_date(date, sizeof date);
    int blen = (int)strlen(text);
    int n = snprintf(head, sizeof head,
                     "HTTP/1.1 %d %s\r\nDate: %s\r\nContent-Type: text/plain; charset=utf-8\r\n"
                     "Content-Length: %d\r\nConnection: close\r\n%s\r\n",
                     code, reason, date, blen, extra);
    write_all(fd, head, (size_t)n);
    if (!head_only)
        write_all(fd, text, (size_t)blen);
}

/* ETag: identifica la VERSIONE del file con ora di modifica e dimensione, fra virgolette (RFC 9110 par. 8.8.3). */
static void make_etag(const struct stat *st, char *out, size_t size)
{
    snprintf(out, size, "\"%llx-%llx\"", (unsigned long long)st->st_mtime, (unsigned long long)st->st_size);
}

/* If-None-Match e' una lista: "\"a\", W/\"b\"" oppure "*". Vero se contiene l'ETag corrente.
 * Per If-None-Match si usa il confronto debole: il prefisso W/ si ignora. */
static int etag_matches(const char *header, const char *etag)
{
    if (header == NULL)
        return 0;
    if (strcmp(header, "*") == 0)
        return 1;
    size_t k = strlen(etag);
    for (const char *p = header; (p = strchr(p, '"')) != NULL;) {
        if (strncmp(p, etag, k) == 0)
            return 1;
        const char *q = strchr(p + 1, '"');         /* salta questo ETag fra virgolette */
        if (q == NULL)
            break;
        p = q + 1;
    }
    return 0;
}

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

static void handle(int fd)
{
    struct rbuf in = {.fd = fd};
    char line[MAX_LINE], method[16], target[MAX_LINE], ver[16];
    if (rb_line(&in, line, sizeof line) < 0 || sscanf(line, "%15s %2047s %15s", method, target, ver) != 3 ||
        strncmp(ver, "HTTP/1.", 7) != 0) {
        reply(fd, 400, "Bad Request", "", "400 Bad Request\n", 0);
        return;
    }
    char auth[512] = "", inm[512] = "";
    int has_auth = 0, has_inm = 0;
    for (;;) {
        int len = rb_line(&in, line, sizeof line);
        if (len < 0) {
            reply(fd, 400, "Bad Request", "", "400 Bad Request\n", 0);
            return;
        }
        if (len == 0)
            break;
        char *colon = strchr(line, ':');
        if (colon == NULL)
            continue;
        *colon = '\0';
        const char *v = colon + 1;
        while (*v == ' ' || *v == '\t')
            v++;
        if (strcasecmp(line, "Authorization") == 0) {
            snprintf(auth, sizeof auth, "%s", v);
            has_auth = 1;
        } else if (strcasecmp(line, "If-None-Match") == 0) {
            snprintf(inm, sizeof inm, "%s", v);
            has_inm = 1;
        }
    }
    int head_only = strcmp(method, "HEAD") == 0;
    if (strcmp(method, "GET") != 0 && !head_only) {
        reply(fd, 405, "Method Not Allowed", "Allow: GET, HEAD\r\n", "405 Method Not Allowed\n", 0);
        return;
    }

    /* /public/NOME o /private/NOME */
    char *q = strchr(target, '?');
    if (q)
        *q = '\0';
    int is_private;
    const char *name;
    if (strncmp(target, "/public/", 8) == 0) {
        is_private = 0;
        name = target + 8;
    } else if (strncmp(target, "/private/", 9) == 0) {
        is_private = 1;
        name = target + 9;
    } else {
        reply(fd, 404, "Not Found", "", "404 Not Found\n", head_only);
        return;
    }
    if (!name_ok(name)) {                          /* niente '/' e niente "..": il percorso non puo' uscire dalla cartella */
        reply(fd, 404, "Not Found", "", "404 Not Found\n", head_only);
        return;
    }

    /* autenticazione: senza credenziali giuste, 401 con la sfida */
    if (is_private && !check_basic(has_auth ? auth : NULL)) {
        reply(fd, 401, "Unauthorized",
              "WWW-Authenticate: Basic realm=\"Area riservata\", charset=\"UTF-8\"\r\n", "401 Unauthorized\n", head_only);
        fprintf(stderr, "%s %s -> 401\n", method, target);
        return;
    }

    char path[MAX_LINE];
    snprintf(path, sizeof path, "%s/%s/%s", root_dir, is_private ? "private" : "public", name);
    int ffd = open(path, O_RDONLY);
    struct stat st;
    if (ffd < 0 || fstat(ffd, &st) < 0 || !S_ISREG(st.st_mode)) {
        if (ffd >= 0)
            close(ffd);
        reply(fd, 404, "Not Found", "", "404 Not Found\n", head_only);
        fprintf(stderr, "%s %s -> 404\n", method, target);
        return;
    }

    char etag[64], date[64], head[1024];
    make_etag(&st, etag, sizeof etag);
    http_date(date, sizeof date);
    /* public: riusabile dalle cache condivise per 60 s. private: no-cache = si puo' memorizzare ma
     * va RIVALIDATO a ogni uso (e' If-None-Match che evita di riscaricare); "private" esclude le cache condivise. */
    const char *cc = is_private ? "private, no-cache" : "public, max-age=60";

    if (has_inm && etag_matches(inm, etag)) {
        /* 304 Not Modified: stessi ETag e Cache-Control, NESSUN corpo */
        int n = snprintf(head, sizeof head,
                         "HTTP/1.1 304 Not Modified\r\nDate: %s\r\nETag: %s\r\nCache-Control: %s\r\nConnection: close\r\n\r\n",
                         date, etag, cc);
        write_all(fd, head, (size_t)n);
        fprintf(stderr, "%s %s -> 304\n", method, target);
        close(ffd);
        return;
    }
    int n = snprintf(head, sizeof head,
                     "HTTP/1.1 200 OK\r\nDate: %s\r\nETag: %s\r\nCache-Control: %s\r\nContent-Type: %s\r\n"
                     "Content-Length: %lld\r\nConnection: close\r\n\r\n",
                     date, etag, cc, mime_type(name), (long long)st.st_size);
    write_all(fd, head, (size_t)n);
    if (!head_only) {
        char buf[8192];
        ssize_t r;
        while ((r = read(ffd, buf, sizeof buf)) > 0)
            if (write_all(fd, buf, (size_t)r) < 0)
                break;
    }
    close(ffd);
    fprintf(stderr, "%s %s -> 200\n", method, target);
}

int main(int argc, char **argv)
{
    if (argc != 4 || strchr(argv[3], ':') == NULL) {
        fprintf(stderr, "uso: %s porta radice utente:password\n", argv[0]);
        return 1;
    }
    root_dir = argv[2];
    credentials = argv[3];
    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(argv[1]));
    if (bind(lfd, (struct sockaddr *)&addr, sizeof addr) < 0 || listen(lfd, 16) < 0) {
        perror("bind/listen");
        return 1;
    }
    for (;;) {
        int cfd = accept(lfd, NULL, NULL);
        if (cfd < 0) {
            if (errno == EINTR)
                continue;
            perror("accept");
            break;
        }
        handle(cfd);
        close(cfd);
    }
    return 0;
}

Compilare e provare

$ mkdir -p www/public www/private
$ echo pubblico > www/public/info.txt && echo segreto > www/private/riservato.txt
$ gcc -Wall -Wextra -o auth_server auth_server.c
$ ./auth_server 8080 ./www mario:segreto &

Senza credenziali:

$ curl -i http://127.0.0.1:8080/private/riservato.txt
HTTP/1.1 401 Unauthorized
Content-Type: text/plain; charset=utf-8
Content-Length: 17
WWW-Authenticate: Basic realm="Area riservata", charset="UTF-8"

401 Unauthorized

Con credenziali (curl -u utente:password costruisce l'header Basic):

$ curl -i -u mario:segreto http://127.0.0.1:8080/private/riservato.txt
HTTP/1.1 200 OK
Date: Sun, 11 Oct 2026 20:24:59 GMT
ETag: "6acbf093-8"
Cache-Control: private, no-cache
Content-Type: text/plain; charset=utf-8
Content-Length: 8
Connection: close

segreto

L'header che curl ha inviato è Authorization: Basic bWFyaW86c2VncmV0bw==; con password sbagliata si ottiene di nuovo 401.

Richiesta condizionale: si prende l'ETag dalla risposta e lo si rimanda.

$ curl -i http://127.0.0.1:8080/public/info.txt
HTTP/1.1 200 OK
ETag: "6acbf093-9"
Cache-Control: public, max-age=60
Content-Length: 9
...
$ curl -i -H 'If-None-Match: "6acbf093-9"' http://127.0.0.1:8080/public/info.txt
HTTP/1.1 304 Not Modified
Date: Sun, 11 Oct 2026 20:24:59 GMT
ETag: "6acbf093-9"
Cache-Control: public, max-age=60
Connection: close

$ touch www/public/info.txt                      # il file cambia: cambia l'ETag
$ curl -i -H 'If-None-Match: "6acbf093-9"' http://127.0.0.1:8080/public/info.txt
HTTP/1.1 200 OK     (ETag nuovo, corpo completo)

(Il valore dell'ETag dipende dall'ora di modifica e dalla dimensione del file.) Altre prove: curl -i --path-as-is http://127.0.0.1:8080/public/../private/riservato.txt → 404 (il nome ../private/riservato.txt non è ammesso; senza --path-as-is curl normalizzerebbe il percorso da solo); curl -I -u mario:segreto .../private/riservato.txt → HEAD: solo gli header; con curl -v si vede che un browser rimanderebbe Authorization a ogni richiesta (HTTP non ha stato).

Spiegazione dei punti chiave

base64_decode (RFC 4648).

  • L'input deve avere lunghezza multipla di 4 (n % 4 != 0 → errore): il Base64 completo è sempre a gruppi di 4 caratteri, con = di riempimento.
  • Per ogni gruppo di 4 caratteri si cerca ciascuno nell'alfabeto A-Za-z0-9+/ (strchr(T, c), che dà il valore da 0 a 63). I 4 valori da 6 bit si ricompongono in un numero a 24 bit: x = v0 << 18 | v1 << 12 | v2 << 6 | v3, da cui si estraggono 3 byte (x >> 16, x >> 8, x).
  • Il = è ammesso solo nelle ultime due posizioni dell'ultimo gruppo: pad = 1 (XXX=) produce 2 byte, pad = 2 (XX==) ne produce 1. Un = in mezzo (TW=u) o un carattere fuori alfabeto dà -1.
  • Il controllo o + 3 > cap evita di scrivere oltre il buffer di uscita.

check_basic.

  • strncasecmp(auth, "Basic ", 6): lo schema non distingue maiuscole, quindi basic è valido.
  • Decodifica il resto in dec[] (max 255 byte), lo termina con '\0' e lo confronta con credentials (utente:password dalla riga di comando).
  • Qualsiasi errore (Base64 non valido, schema diverso, assenza dell'header) → non autenticato.

secure_equal. Confronta due stringhe senza fermarsi alla prima differenza: accumula con |= le differenze di ogni byte (più la differenza di lunghezza) e controlla alla fine. Una strcmp risponde più in fretta quando la differenza è nei primi caratteri: misurando i tempi un attaccante può ricavare la password un carattere alla volta (timing attack). Qui il tempo non dipende da dove si differenzia.

401 e WWW-Authenticate. Se il percorso è /private/ e check_basic fallisce, la risposta è 401 con la sfida Basic realm="Area riservata", charset="UTF-8" (RFC 7617: il parametro charset dice che le credenziali sono UTF-8). Si distingue da 403 Forbidden ("ti ho riconosciuto, ma non hai diritto"): 401 invita a riprovare con le credenziali. Per HEAD il corpo non si invia.

name_ok e il percorso. L'URL è /public/NOME o /private/NOME, e NOME può contenere solo lettere, cifre, _, -, ., e non iniziare con .: niente /, quindi niente sottocartelle né ... È un modo per costruzione di impedire il path traversal, più semplice del controllo con la decodifica di Esercizio - Server HTTP iterativo con GET, HEAD e codici di errore (sul modello della prova pratica). Si controlla prima l'autenticazione dell'area e poi l'esistenza del file: così un utente non autenticato non scopre quali file esistono (un 404 prima del 401 lo rivelerebbe).

make_etag. "%llx-%llx" con data di modifica e dimensione: "6acbf093-8" significa st_mtime = 0x6acbf093 e 8 byte. Le virgolette fanno parte dell'ETag. Cambia a ogni modifica del file (data o dimensione), e non serve calcolare un hash del contenuto.

etag_matches. If-None-Match può contenere una lista ("a", W/"b") o *. Il codice cerca ogni stringa fra virgolette e la confronta con l'ETag corrente (con strncmp sulla lunghezza); il prefisso W/ (validatore debole) viene ignorato perché If-None-Match usa il confronto debole (RFC 9110 par. 8.8.3.2); se trova una corrispondenza o c'è * → 304.

Il 304. Si inviano Date, ETag, Cache-Control e Connection: close e nessun corpo né Content-Length/Content-Type. Il client riusa la copia che ha già e la considera fresca per il tempo indicato.

Cache-Control. /public/ ha public, max-age=60: qualunque cache (anche un proxy) può riusarla per 60 secondi senza contattare il server. /private/ ha private, no-cache: private esclude le cache condivise (i proxy non devono tenere contenuto riservato) e no-cache obbliga a rivalidare ogni volta (il client chiede con If-None-Match, e riceve un 304 economico). Non è no-store: la copia può esistere nella cache del browser ma va sempre rivalidata.

Errori tipici

  • Rispondere 401 senza l'header WWW-Authenticate: il browser non mostra la finestra delle credenziali.
  • Rispondere 403 quando mancano le credenziali (è 401), o 401 quando le credenziali sono giuste ma mancano i diritti.
  • Credere che Base64 protegga la password: solo TLS la protegge.
  • Confrontare le credenziali con strcmp (timing attack) o decodificarle senza controllare la lunghezza del buffer.
  • Inviare un corpo (o Content-Length) in un 304.
  • Dimenticare le virgolette dell'ETag nel confronto, o confrontare con If-Modified-Since come se fosse un ETag.
  • Rispondere 304 quando l'ETag non corrisponde, o controllare If-None-Match prima dell'autenticazione: si rivelerebbe l'esistenza del file.
  • Mettere no-cache credendo che vieti la memorizzazione (il divieto è no-store).
  • Ammettere nomi con / o .. nell'URL.
  • Rispondere 404 prima di 401 per i file privati.

Varianti per esercitarsi

Versione ripasso

  • Testo. ./auth_server porta radice utente:password; /public/NOME libero con public, max-age=60; /private/NOME con Basic (401 + WWW-Authenticate: Basic realm="Area riservata", charset="UTF-8" altrimenti) e private, no-cache; ETag e If-None-Match -> 304 senza corpo; Base64, confronto a tempo costante, nomi semplici.
  • Autenticazione. Client: senza credenziali -> 401 + sfida; poi Authorization: Basic base64("utente:password") (mario:segreto = bWFyaW86c2VncmV0bw==). Basic non cifra: solo con TLS. 401 = servono credenziali; 403 = non hai diritto.
  • base64_decode. Lunghezza multipla di 4; 4 caratteri -> 4 valori da 6 bit (strchr nell'alfabeto) -> x = v0 << 18 | v1 << 12 | v2 << 6 | v3 -> 3 byte (x >> 16, x >> 8, x); = solo in fondo (XXX= -> 2 byte, XX== -> 1); -1 su carattere non valido; controllo o + 3 > cap.
  • check_basic. strncasecmp(auth, "Basic ", 6) (schema case-insensitive); decodifica in dec[], dec[n] = '\0'; secure_equal(dec, credentials).
  • secure_equal. diff = lenA ^ lenB; for ... diff |= a[i] ^ b[i]; return diff == 0;: tempo indipendente da dove differiscono (contro il timing attack; strcmp si ferma alla prima differenza).
  • ETag e cache. ETag: "mtime_hex-size_hex" (virgolette incluse; cambia se il file cambia). If-None-Match: lista di ETag o *, confronto debole (si ignora W/); uguale -> 304 (Date, ETag, Cache-Control, Connection, niente corpo né Content-Length); diverso -> 200.
  • Cache-Control. public, max-age=60: riusabile da tutte le cache per 60 s senza richieste. private, no-cache: non nelle cache condivise e da rivalidare ogni volta (non è no-store).
  • Percorso. /public/NOME o /private/NOME con NOME fatto di lettere, cifre, _ - . (non . iniziale): niente /, niente .. per costruzione. Prima l'autenticazione, poi l'esistenza del file (un 404 prima del 401 rivelerebbe i file).
  • Prove. curl -i .../private/riservato.txt -> 401; curl -i -u mario:segreto ... -> 200 + ETag; curl -i -H 'If-None-Match: "6acbf093-9"' .../public/info.txt -> 304; dopo touch -> 200.
  • Codice essenziale (le funzioni centrali, senza commenti):
c
static int base64_decode(const char *in, uint8_t *out, size_t cap)
{
    static const char T[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
    size_t n = strlen(in), o = 0;
    if (n == 0 || n % 4 != 0)
        return -1;
    for (size_t i = 0; i < n; i += 4) {
        int v[4], pad = 0;
        for (int k = 0; k < 4; k++) {
            char c = in[i + (size_t)k];
            if (c == '=' && i + 4 == n && k >= 2) {
                v[k] = 0;
                pad++;
            } else {
                const char *p = (c == '\0' || pad > 0) ? NULL : strchr(T, c);
                if (p == NULL)
                    return -1;
                v[k] = (int)(p - T);
            }
        }
        uint32_t x = (uint32_t)v[0] << 18 | (uint32_t)v[1] << 12 | (uint32_t)v[2] << 6 | (uint32_t)v[3];
        if (o + 3 > cap)
            return -1;
        out[o++] = (uint8_t)(x >> 16);
        if (pad < 2)
            out[o++] = (uint8_t)(x >> 8);
        if (pad < 1)
            out[o++] = (uint8_t)x;
    }
    return (int)o;
}

static int check_basic(const char *auth)
{
    if (auth == NULL || strncasecmp(auth, "Basic ", 6) != 0)
        return 0;
    const char *b64 = auth + 6;
    while (*b64 == ' ')
        b64++;
    uint8_t dec[256];
    int n = base64_decode(b64, dec, sizeof dec - 1);
    if (n < 0)
        return 0;
    dec[n] = '\0';
    return secure_equal((const char *)dec, credentials);
}

static int secure_equal(const char *a, const char *b)
{
    size_t la = strlen(a), lb = strlen(b);
    unsigned diff = (unsigned)(la ^ lb);
    for (size_t i = 0; i < la && i < lb; i++)
        diff |= (unsigned)(a[i] ^ b[i]);
    return diff == 0;
}

static int etag_matches(const char *header, const char *etag)
{
    if (header == NULL)
        return 0;
    if (strcmp(header, "*") == 0)
        return 1;
    size_t k = strlen(etag);
    for (const char *p = header; (p = strchr(p, '"')) != NULL;) {
        if (strncmp(p, etag, k) == 0)
            return 1;
        const char *q = strchr(p + 1, '"');
        if (q == NULL)
            break;
        p = q + 1;
    }
    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 make_etag(const struct stat *st, char *out, size_t size)
{
    snprintf(out, size, "\"%llx-%llx\"", (unsigned long long)st->st_mtime, (unsigned long long)st->st_size);
}

static void reply(int fd, int code, const char *reason, const char *extra, const char *text, int head_only)
{
    char head[1024], date[64];
    http_date(date, sizeof date);
    int blen = (int)strlen(text);
    int n = snprintf(head, sizeof head,
                     "HTTP/1.1 %d %s\r\nDate: %s\r\nContent-Type: text/plain; charset=utf-8\r\n"
                     "Content-Length: %d\r\nConnection: close\r\n%s\r\n",
                     code, reason, date, blen, extra);
    write_all(fd, head, (size_t)n);
    if (!head_only)
        write_all(fd, text, (size_t)blen);
}
  • Errori tipici: 401 senza WWW-Authenticate; 403 al posto di 401; Base64 creduto protezione; strcmp sulle credenziali; corpo in un 304; virgolette dell'ETag dimenticate; no-cache creduto divieto; nomi con / o ..; 404 prima del 401.

Esercizi su questo argomento

Teoria collegata