Esercizio - Client HTTP 1.1 con decodifica del chunked (slide HTTP 1.1)
In questa pagina 7
Testo (slide HTTP 1.1 del corso di Reti di Calcolatori, Ing. Informatica UniPD: «Client HTTP/1.1: Chunked»).
Scrivere un client HTTP/1.1 che gestisca le risposte con Transfer-Encoding: chunked:
- leggere i chunk (dimensione in esadecimale, dati, CRLF) e scrivere nel file o su
stdoutsolo i dati, senza le righe con le dimensioni; - fermarsi al chunk di lunghezza zero e leggere gli eventuali trailer fino alla riga vuota;
- funzionare anche quando i dati di un chunk contengono CRLF o byte zero, quando la dimensione ha cifre maiuscole o è seguita da estensioni (
1A;nome=valore); - gestire, con lo stesso programma, anche le risposte con
Content-Lengthe quelle terminate dalla chiusura della connessione (priorità:Transfer-EncodingbatteContent-Length, RFC 9112 par. 6.3).
Teoria: 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 →, Esercizio - Client HTTP 1.1 con Content-Length e connessione persistente (slide HTTP 1.1), Richiami di C per la programmazione di rete - memoria, puntatori, struct ed endiannessIn C un programma di rete maneggia byte, non oggetti: un processo ha codice, dati statici, heap e stack; i tipi hanno dimensioni fisse solo se si usano <stdint.h> (uint8_t, uint16_t, uint32_t); i dati che arrivano da un socket sono un buffer di byte con una lunghezza, NON una stringa C terminata da '\0'; i puntatori e l'aritmetica dei puntatori (buf + totale) permettono di riempire un buffer a pezzi; una struct puo' contenere byte di riempimento (padding) per l'allineamento, quindi non si spedisce con write(&s, sizeof s); sulla rete i numeri a piu' byte viaggiano in big endian (network byte order) e si convertono con htons, htonl, ntohs, ntohl, oppure si serializzano a mano con shift e maschere.Richiami di C per la programmazione di rete - memoria, puntatori, struct ed endianness →.
Idea
Il formato (RFC 9112 par. 7.1):
chunked-body = *chunk last-chunk trailer-section CRLF
chunk = chunk-size [ chunk-ext ] CRLF chunk-data CRLF chunk-size = cifre ESADECIMALI
last-chunk = 1*("0") [ chunk-ext ] CRLFPer decodificarlo bisogna leggere in due modi diversi dalla stessa connessione:
- le righe della dimensione e dei trailer si leggono a righe (fino al CRLF);
- i dati di un chunk si leggono a byte, in numero esatto (
chunk-size), senza interpretarli.
Per questo il client usa un lettore con buffer (struct rbuf) con tre operazioni sullo stesso buffer: rb_line (una riga), rb_read (fino a n byte già disponibili) e rb_fill (rilegge dal socket se il buffer è vuoto). Se le righe fossero lette con una read diretta e i dati con un'altra, i byte già letti nel buffer andrebbero persi.
L'algoritmo di read_chunked:
ciclo:
riga = rb_line() es. "1A;nome=valore"
size = strtoul(riga, base 16) si ferma alla ';': l'estensione si ignora
se size == 0: esci dal ciclo ultimo chunk
copia ESATTAMENTE size byte nell'uscita (anche se contengono CRLF)
rb_line() deve restituire la riga vuota (il CRLF dopo i dati)
poi: righe di trailer fino alla riga vuotaIl punto debole del codice delle slide. Il codice delle slide legge un byte alla volta e riconosce il confine fra dimensione e dati cercando il prossimo CRLF, senza usare size per contare. Va bene per una risposta i cui dati non contengono mai CRLF, ma si rompe appena un chunk contiene un a-capo \r\n (una pagina HTML con righe lo contiene): quel CRLF viene preso per la fine del chunk e la riga successiva, che è testo, viene letta come dimensione. Qui il chunk 1 contiene due CRLF proprio per mostrarlo.
Codice
/* client11_chunked.c - client HTTP/1.1 che decodifica Transfer-Encoding: chunked (e anche Content-Length
* e corpo chiuso dalla connessione). Il corpo decodificato va su stdout o nel file indicato.
*
* Compilare: gcc -Wall -Wextra -o client11_chunked client11_chunked.c
* Usare: ./client11_chunked host porta percorso [file_uscita]
* Diagnostica (numero e dimensione dei chunk, trailer) su stderr.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#define MAX_LINE 4096
#define MAX_CHUNK (64L * 1024 * 1024) /* rifiuta chunk assurdi (RFC 9112 7.1: attenzione agli overflow) */
/* ---------- lettore con buffer: righe E byte grezzi dallo stesso buffer ---------- */
struct rbuf {
int fd;
char buf[4096];
size_t pos, len;
};
/* Riempie il buffer se e' vuoto. Torna 0 se ci sono byte disponibili, -1 su EOF/errore. */
static int rb_fill(struct rbuf *r)
{
while (r->pos == r->len) {
ssize_t k = read(r->fd, r->buf, sizeof r->buf);
if (k < 0 && errno == EINTR)
continue;
if (k <= 0)
return -1;
r->pos = 0;
r->len = (size_t)k;
}
return 0;
}
/* Come fgets: una riga con CRLF o LF tolti. Torna la lunghezza, -1 su EOF o riga troppo lunga. */
static int rb_line(struct rbuf *r, char *line, size_t max)
{
size_t n = 0;
for (;;) {
if (rb_fill(r) < 0)
return -1;
char c = r->buf[r->pos++];
if (c == '\n') {
if (n > 0 && line[n - 1] == '\r')
n--;
line[n] = '\0';
return (int)n;
}
if (n + 1 >= max)
return -1;
line[n++] = c;
}
}
/* Copia fino a n byte (quelli disponibili ora) dal buffer a dst. Torna quanti, -1 su EOF. */
static ssize_t rb_read(struct rbuf *r, char *dst, size_t n)
{
if (rb_fill(r) < 0)
return -1;
size_t avail = r->len - r->pos;
if (n > avail)
n = avail;
memcpy(dst, r->buf + r->pos, n);
r->pos += n;
return (ssize_t)n;
}
/* ---------- rete ---------- */
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;
}
/* ---------- corpo ---------- */
/* Legge un corpo chunked e scrive i dati (senza le lunghezze) in out. Torna i byte di dati, -1 su errore.
* chunked-body = *chunk last-chunk trailer-section CRLF
* chunk = chunk-size [ chunk-ext ] CRLF chunk-data CRLF (chunk-size in ESADECIMALE) */
static long read_chunked(struct rbuf *r, FILE *out)
{
char line[MAX_LINE], buf[4096];
long total = 0;
int nchunks = 0;
for (;;) {
if (rb_line(r, line, sizeof line) < 0)
return -1;
/* "1a;nome=valore": le estensioni dopo ';' si ignorano. strtoul legge solo la parte esadecimale. */
char *end;
errno = 0;
unsigned long size = strtoul(line, &end, 16);
if (end == line || errno == ERANGE || size > (unsigned long)MAX_CHUNK ||
!(*end == '\0' || *end == ';' || *end == ' ' || *end == '\t')) {
fprintf(stderr, "chunk-size non valido: '%s'\n", line);
return -1;
}
if (size == 0)
break; /* last-chunk: finiscono i dati */
nchunks++;
fprintf(stderr, "chunk %d: %lu byte\n", nchunks, size);
/* chunk-data: ESATTAMENTE 'size' byte, che possono contenere anche CRLF o zeri: non vanno interpretati */
size_t left = size;
while (left > 0) {
ssize_t k = rb_read(r, buf, left < sizeof buf ? left : sizeof buf);
if (k < 0)
return -1;
fwrite(buf, 1, (size_t)k, out);
left -= (size_t)k;
}
total += (long)size;
/* CRLF che chiude il chunk */
if (rb_line(r, line, sizeof line) < 0 || line[0] != '\0') {
fprintf(stderr, "manca il CRLF dopo il chunk\n");
return -1;
}
}
/* trailer-section: righe "Nome: valore" fino alla riga vuota (di solito non ce ne sono) */
for (;;) {
if (rb_line(r, line, sizeof line) < 0)
return -1;
if (line[0] == '\0')
break;
fprintf(stderr, "trailer: %s\n", line);
}
return total;
}
int main(int argc, char **argv)
{
if (argc < 4 || argc > 5) {
fprintf(stderr, "uso: %s host porta percorso [file_uscita]\n", argv[0]);
return 1;
}
int fd = connect_to(argv[1], argv[2]);
if (fd < 0) {
fprintf(stderr, "connessione fallita\n");
return 1;
}
char req[2048];
int n = snprintf(req, sizeof req,
"GET %s HTTP/1.1\r\nHost: %s:%s\r\nUser-Agent: client11_chunked/1.0\r\nConnection: close\r\n\r\n",
argv[3], argv[1], argv[2]);
if (write_all(fd, req, (size_t)n) < 0) {
perror("write");
return 1;
}
struct rbuf rb = {.fd = fd};
char line[MAX_LINE];
/* status-line */
int major, minor, status;
if (rb_line(&rb, line, sizeof line) < 0 || sscanf(line, "HTTP/%d.%d %d", &major, &minor, &status) != 3) {
fprintf(stderr, "risposta non valida\n");
return 1;
}
fprintf(stderr, "stato %d\n", status);
/* header: ci servono Transfer-Encoding e Content-Length */
int chunked = 0;
long content_length = -1;
for (;;) {
int len = rb_line(&rb, line, sizeof line);
if (len < 0) {
fprintf(stderr, "risposta interrotta\n");
return 1;
}
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, "Transfer-Encoding") == 0 && strstr(v, "chunked") != NULL)
chunked = 1;
else if (strcasecmp(line, "Content-Length") == 0)
content_length = atol(v);
}
FILE *out = (argc == 5) ? fopen(argv[4], "wb") : stdout;
if (out == NULL) {
perror("fopen");
return 1;
}
/* RFC 9112 par. 6.3: se c'e' Transfer-Encoding chunked, ha la precedenza su Content-Length */
long total;
if (chunked) {
total = read_chunked(&rb, out);
} else if (content_length >= 0) {
total = 0;
char buf[4096];
while (total < content_length) {
long want = content_length - total;
ssize_t k = rb_read(&rb, buf, (size_t)want < sizeof buf ? (size_t)want : sizeof buf);
if (k < 0) {
total = -1;
break;
}
fwrite(buf, 1, (size_t)k, out);
total += k;
}
} else {
total = 0; /* corpo chiuso dalla connessione */
char buf[4096];
ssize_t k;
while ((k = rb_read(&rb, buf, sizeof buf)) > 0) {
fwrite(buf, 1, (size_t)k, out);
total += k;
}
}
if (out != stdout)
fclose(out);
close(fd);
if (total < 0) {
fprintf(stderr, "corpo incompleto o malformato\n");
return 1;
}
fprintf(stderr, "[%s, %ld byte di dati]\n", chunked ? "chunked" : (content_length >= 0 ? "Content-Length" : "fino alla chiusura"), total);
return 0;
}Server di prova
Per provare il client senza internet serve un server che risponda con un corpo chunked "difficile": questo piccolo programma Python invia, a qualunque richiesta, tre chunk (il primo con due CRLF nei dati e la dimensione in maiuscolo con un'estensione) e un trailer.
"""chunked_test_server.py - server di prova che risponde SEMPRE con un corpo chunked "difficile":
un chunk con dentro CRLF, una estensione di chunk, la dimensione in esadecimale maiuscolo e un trailer.
Avvio: python3 chunked_test_server.py 8001
"""
import socketserver
import sys
# i dati dei tre chunk: il primo contiene due CRLF (una pagina HTML con le righe a-capo li ha)
C1 = b"Prima riga\r\nseconda riga\r\n" # 26 byte = 0x1A
C2 = b"0123456789ABCDEF" # 16 byte = 0x10
C3 = b"fine" # 4 byte
BODY = (
b"1A;nome=valore\r\n" + C1 + b"\r\n" # dimensione maiuscola + estensione
+ b"10\r\n" + C2 + b"\r\n"
+ b"4\r\n" + C3 + b"\r\n"
+ b"0\r\nX-Totale: 46\r\n\r\n" # chunk finale + trailer + riga vuota
)
HEAD = (
b"HTTP/1.1 200 OK\r\n"
b"Content-Type: text/plain\r\n"
b"Transfer-Encoding: chunked\r\n"
b"Connection: close\r\n"
b"\r\n"
)
class Handler(socketserver.StreamRequestHandler):
def handle(self):
while self.rfile.readline().strip(): # legge e scarta la richiesta fino alla riga vuota
pass
self.wfile.write(HEAD + BODY)
if __name__ == "__main__":
socketserver.TCPServer.allow_reuse_address = True
with socketserver.TCPServer(("127.0.0.1", int(sys.argv[1])), Handler) as srv:
srv.serve_forever()Compilare e provare
$ gcc -Wall -Wextra -o client11_chunked client11_chunked.c
$ python3 chunked_test_server.py 8001 &
$ ./client11_chunked 127.0.0.1 8001 / risposta.txt
stato 200
chunk 1: 26 byte
chunk 2: 16 byte
chunk 3: 4 byte
trailer: X-Totale: 46
[chunked, 46 byte di dati]
$ od -c risposta.txt | head -3
0000000 P r i m a r i g a \r \n s e c o
0000020 n d a r i g a \r \n 0 1 2 3 4 5
0000040 6 7 8 9 A B C D E F f i n eIl file contiene i 46 byte di dati (26 + 16 + 4), con i due CRLF del primo chunk intatti e senza le righe 1A;nome=valore, 10, 4. Altre prove:
- contro un server vero che usa il chunked (molti siti con risposte dinamiche):
./client11_chunked www.example.com 80 /; - con il server dell'esercizio Esercizio - Server HTTP che risponde in chunked con trailer (sul modello della prova pratica):
./client11_chunked 127.0.0.1 8080 /loreme/stream; - con un server con
Content-Lengtho con chiusura: il programma stampa[Content-Length, ...]o[fino alla chiusura, ...].
Risposte volutamente sbagliate (da provare con nc -l o con il server Python modificato): un chunk troncato (5\r\nabc e poi chiusura) produce corpo incompleto o malformato; una dimensione non esadecimale (ZZ) produce chunk-size non valido: 'ZZ'.
Spiegazione dei punti chiave
rb_fill, rb_line, rb_read.
rb_fill: se il buffer è vuoto (pos == len) fa unareadda 4 KiB (riprova suEINTR);0byte o errore →-1.rb_line: prende byte dal buffer fino al\n, toglie un\rche lo precede; protetta contro righe troppo lunghe (-1).rb_read(r, dst, n): restituisce fino anbyte, ma solo quelli già nel buffer (riempie solo se è vuoto): chi chiama ripete finché ne ha ricevutin.
read_chunked.
strtoul(line, &end, 16): legge cifre esadecimali (minuscole o maiuscole) e si ferma al primo carattere non valido. Poi si controlla che sia\0,;, spazio o tabulazione:1A;nome=valoreè valido,1AZno.errno == ERANGEe il limiteMAX_CHUNKproteggono da dimensioni enormi (RFC 9112 par. 7.1: il ricevente deve prevedere numeri esadecimali molto grandi e non farli andare in overflow).size == 0è l'ultimo chunk: si esce dal ciclo.- I dati:
while (left > 0) { k = rb_read(r, buf, min(left, sizeof buf)); fwrite(buf, 1, k, out); left -= k; }.fwritecon la lunghezza (nonfputs) perché i dati sono binari. - Dopo i dati ci deve essere un CRLF:
rb_linedeve restituire la riga vuota; altrimenti la risposta è malformata (-1). - Dopo l'ultimo chunk, le righe di trailer (
Nome: valore) fino alla riga vuota. Il client le stampa (un hash o un conteggio calcolati dal server durante l'invio).
Ordine di priorità (nel main). Se chunked, si usa read_chunked anche se c'è Content-Length (prova: il server Python manda solo il chunked, ma una risposta con entrambi viene letta come chunked); altrimenti con Content-Length si leggono quei byte; altrimenti fino alla chiusura.
Richiesta con Connection: close. Qui è comoda: dopo il corpo il server chiude e il client termina in ogni caso. Con una connessione persistente basta non chiudere e riusare il lettore rbuf (il buffer può contenere già l'inizio della risposta successiva).
Che cosa non fa. Non gestisce la decompressione (Content-Encoding: gzip), non riusa la connessione, non verifica che il trailer corrisponda a quanto annunciato in Trailer.
Errori tipici
- Cercare CRLF per trovare la fine di un chunk invece di contare
chunk-sizebyte: si rompe con dati che contengono CRLF. - Convertire la dimensione come decimale (
atoi,%d) invece che esadecimale (b= 11,14= 20,1A= 26). - Includere le righe con le dimensioni nel contenuto.
- Dimenticare il CRLF dopo i dati o la riga vuota finale: il client legge un byte in più, o resta in attesa.
- Non tollerare estensioni (
;nome=valore) o maiuscole nella dimensione. - Ignorare il trailer: il parser interpreta le sue righe come l'inizio della risposta successiva (connessione persistente).
- Usare
fputs/strlensui dati. - Convertire dimensioni con
intsenza controlli: una dimensione molto lunga provoca overflow. - Leggere i dati con una
readdiretta dopo aver letto le righe con il buffer (i byte nel buffer vanno persi).
Varianti per esercitarsi
- Unire questo client al precedente (Esercizio - Client HTTP 1.1 con Content-Length e connessione persistente (slide HTTP 1.1)): connessione persistente con
Content-Lengthe chunked. - Stampare il corpo mentre arriva (streaming) senza accumulare tutto.
- Accettare
Content-Encoding: gzipdecomprimendo conzlib.
Versione ripasso
- Testo. Client HTTP/1.1 che decodifica
Transfer-Encoding: chunked(solo dati nell'uscita), si ferma al chunk0, legge i trailer; funziona con CRLF nei dati, dimensioni maiuscole, estensioni; gestisce ancheContent-Lengthe corpo fino alla chiusura (Transfer-Encodingha la precedenza). - Formato.
chunk-size(esadecimale) [;ext] CRLF,chunk-sizebyte di dati, CRLF; ripetuto;0[;ext] CRLF; trailer (Nome: valore); riga vuota. - Lettore con buffer.
rb_fill(unareadda 4 KiB se vuoto),rb_line(riga senza CRLF),rb_read(n)(fino anbyte già disponibili): righe e dati dallo stesso buffer.
for (;;) {
rb_line(r, line, sizeof line); /* "1A;nome=valore" */
unsigned long size = strtoul(line, &end, 16); /* base 16, si ferma a ';' */
if (end == line || errno == ERANGE || size > MAX_CHUNK) return -1;
if (size == 0) break; /* ultimo chunk */
for (left = size; left > 0; left -= k) { k = rb_read(r, buf, min(left, sizeof buf)); fwrite(buf, 1, k, out); }
rb_line(r, line, sizeof line); /* CRLF dopo i dati: riga vuota */
}
while (rb_line(r, line, sizeof line) > 0) puts(line); /* trailer fino alla riga vuota */- Controlli. Dopo
strtoul:\0,;, spazio o tab;errno == ERANGEeMAX_CHUNKcontro l'overflow; riga dopo i dati vuota;fwritecon la lunghezza (dati binari). - Punto debole delle slide. Riconoscere il fine chunk cercando CRLF (senza contare
size) si rompe con dati che contengono CRLF. - Prove.
python3 chunked_test_server.py 8001;./client11_chunked 127.0.0.1 8001 / risposta.txt->chunk 1: 26 byte,chunk 2: 16,chunk 3: 4,trailer: X-Totale: 46,[chunked, 46 byte di dati]; file di 46 byte con i due CRLF intatti.5\r\nabc+ chiusura ->corpo incompleto o malformato;ZZ->chunk-size non valido. - Codice essenziale (le funzioni centrali, senza commenti):
static int rb_fill(struct rbuf *r)
{
while (r->pos == r->len) {
ssize_t k = read(r->fd, r->buf, sizeof r->buf);
if (k < 0 && errno == EINTR)
continue;
if (k <= 0)
return -1;
r->pos = 0;
r->len = (size_t)k;
}
return 0;
}
static int rb_line(struct rbuf *r, char *line, size_t max)
{
size_t n = 0;
for (;;) {
if (rb_fill(r) < 0)
return -1;
char c = r->buf[r->pos++];
if (c == '\n') {
if (n > 0 && line[n - 1] == '\r')
n--;
line[n] = '\0';
return (int)n;
}
if (n + 1 >= max)
return -1;
line[n++] = c;
}
}
static ssize_t rb_read(struct rbuf *r, char *dst, size_t n)
{
if (rb_fill(r) < 0)
return -1;
size_t avail = r->len - r->pos;
if (n > avail)
n = avail;
memcpy(dst, r->buf + r->pos, n);
r->pos += n;
return (ssize_t)n;
}
static long read_chunked(struct rbuf *r, FILE *out)
{
char line[MAX_LINE], buf[4096];
long total = 0;
int nchunks = 0;
for (;;) {
if (rb_line(r, line, sizeof line) < 0)
return -1;
char *end;
errno = 0;
unsigned long size = strtoul(line, &end, 16);
if (end == line || errno == ERANGE || size > (unsigned long)MAX_CHUNK ||
!(*end == '\0' || *end == ';' || *end == ' ' || *end == '\t')) {
fprintf(stderr, "chunk-size non valido: '%s'\n", line);
return -1;
}
if (size == 0)
break;
nchunks++;
fprintf(stderr, "chunk %d: %lu byte\n", nchunks, size);
size_t left = size;
while (left > 0) {
ssize_t k = rb_read(r, buf, left < sizeof buf ? left : sizeof buf);
if (k < 0)
return -1;
fwrite(buf, 1, (size_t)k, out);
left -= (size_t)k;
}
total += (long)size;
if (rb_line(r, line, sizeof line) < 0 || line[0] != '\0') {
fprintf(stderr, "manca il CRLF dopo il chunk\n");
return -1;
}
}
for (;;) {
if (rb_line(r, line, sizeof line) < 0)
return -1;
if (line[0] == '\0')
break;
fprintf(stderr, "trailer: %s\n", line);
}
return total;
}- Errori tipici: fine chunk cercata con CRLF; dimensione letta in decimale; righe delle dimensioni nel contenuto; manca il CRLF dopo i dati o la riga vuota finale; estensioni o maiuscole rifiutate; trailer ignorato;
fputs/strlensui dati; overflow della dimensione;readdiretta mescolata al buffer.