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:
GET /public/NOME: file liberi, conCache-Control: public, max-age=60;GET /private/NOME: richiedono autenticazione Basic; senza credenziali giuste il server risponde401 UnauthorizedconWWW-Authenticate: Basic realm="Area riservata"; con quelle giuste serve il file conCache-Control: private, no-cache;- per tutti i file: header
ETag(identificatore della versione) e supporto aIf-None-Match: se l'ETag del client coincide con quello corrente,304 Not Modifiedsenza corpo, conETageCache-Control; - decodificare Base64 (
Authorization: Basic ...) e confrontare le credenziali in tempo costante; - 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
/* 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 UnauthorizedCon 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
segretoL'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 > capevita di scrivere oltre il buffer di uscita.
check_basic.
strncasecmp(auth, "Basic ", 6): lo schema non distingue maiuscole, quindibasicè valido.- Decodifica il resto in
dec[](max 255 byte), lo termina con'\0'e lo confronta concredentials(utente:passworddalla 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
401senza l'headerWWW-Authenticate: il browser non mostra la finestra delle credenziali. - Rispondere
403quando mancano le credenziali (è401), o401quando 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 un304. - Dimenticare le virgolette dell'ETag nel confronto, o confrontare con
If-Modified-Sincecome se fosse un ETag. - Rispondere
304quando l'ETag non corrisponde, o controllareIf-None-Matchprima dell'autenticazione: si rivelerebbe l'esistenza del file. - Mettere
no-cachecredendo che vieti la memorizzazione (il divieto èno-store). - Ammettere nomi con
/o..nell'URL. - Rispondere
404prima di401per i file privati.
Varianti per esercitarsi
- Supportare anche
If-Modified-Since(servestrptime/timegmper la data) e rispondere304quando il file non è più recente. - Calcolare l'ETag come hash del contenuto (per esempio SHA-1 dal client WebSocket: Esercizio - Client WebSocket con handshake, SHA-1 e Base64 (sul modello della prova pratica)).
- Passare all'autenticazione Digest (RFC 7616): nonce,
HA1,HA2(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 →). - Elenco di utenti da un file, con le password salvate come hash.
- Rispondere a
Rangecon206eAccept-Ranges: bytes. - Unire il codice al server concorrente (Esercizio - Server HTTP concorrente con fork e connessioni persistenti (sul modello della prova pratica)).
Versione ripasso
- Testo.
./auth_server porta radice utente:password;/public/NOMElibero conpublic, max-age=60;/private/NOMEcon Basic (401+WWW-Authenticate: Basic realm="Area riservata", charset="UTF-8"altrimenti) eprivate, no-cache;ETageIf-None-Match->304senza corpo; Base64, confronto a tempo costante, nomi semplici. - Autenticazione. Client: senza credenziali ->
401+ sfida; poiAuthorization: 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 (strchrnell'alfabeto) ->x = v0 << 18 | v1 << 12 | v2 << 6 | v3-> 3 byte (x >> 16,x >> 8,x);=solo in fondo (XXX=-> 2 byte,XX==-> 1);-1su carattere non valido; controlloo + 3 > cap.check_basic.strncasecmp(auth, "Basic ", 6)(schema case-insensitive); decodifica indec[],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;strcmpsi 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 ignoraW/); 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/NOMEo/private/NOMEconNOMEfatto di lettere, cifre,_ - .(non.iniziale): niente/, niente..per costruzione. Prima l'autenticazione, poi l'esistenza del file (un404prima del401rivelerebbe 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; dopotouch->200. - Codice essenziale (le funzioni centrali, senza commenti):
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;
}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:
401senzaWWW-Authenticate;403al posto di401; Base64 creduto protezione;strcmpsulle credenziali; corpo in un304; virgolette dell'ETag dimenticate;no-cachecreduto divieto; nomi con/o..;404prima del401.
Esercizi su questo argomento
- Esercizio - GET condizionale con If-Modified-Since e cache su file (slide HTTP 0.9 e 1.0)
- Esercizio - Proxy con tunnel CONNECT (sul modello della prova pratica)
- Esercizio - Server HTTP concorrente con fork e connessioni persistenti (sul modello della prova pratica)
- Esercizio - Server HTTP iterativo con GET, HEAD e codici di errore (sul modello della prova pratica)
- Esercizio - Servizio REST con JSON (sul modello della prova pratica)