Esercizio - Parsing di request line, header e URI (sul modello della prova pratica)
In questa pagina 6
Testo (sul modello della prova pratica di Reti di Calcolatori, Ing. Informatica UniPD).
Una richiesta HTTP è già stata ricevuta e si trova in un buffer in memoria (buf, len). Scrivere in C le funzioni che la analizzano:
parse_request(buf, len, &req)deve estrarre metodo, target (percorso decodificato e query), versione, gli header (nomi senza distinzione di maiuscole), la lunghezza del corpo (Content-Length), se il corpo èchunked, e se la connessione resta aperta (keep-alive). Deve restituire0se la richiesta è valida, un indicatore se è incompleta (servono altri byte) oppure il codice di stato HTTP da rispondere in caso di errore (400,414,431,505).url_decodeepath_is_safe: decodificare il percent-encoding e rifiutare i percorsi che escono dalla radice (..).parse_url: dividere un URL assolutohttp://host[:porta][/percorso](come lo scrive un client a un proxy).- Verificare tutto con un programma di prova.
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 →, 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 →, 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 →, 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 parsing è la parte di un server HTTP dove si nascondono quasi tutti gli errori e le vulnerabilità. Conviene separarlo dalla rete: una funzione che riceve byte già in memoria e restituisce una struttura è facile da provare (basta una stringa) e da riusare in un server, in un proxy o in un CGI. Il programma sotto non apre socket: ha un main che esegue gli autotest.
- Righe: terminate da CRLF, ma si accetta anche il solo LF (RFC 9112 par. 2.2).
next_linerestituisce una riga alla volta e tre risultati: riga intera, riga incompleta (nel buffer manca ancora il terminatore: bisogna leggere altri byte dal socket), riga troppo lunga. - Request line:
METODO SP TARGET SP HTTP/x.y, con esattamente due spazi. - Header:
Nome: valorecon il nome senza spazi prima dei due punti (RFC 9112 par. 5.1 impone di rifiutarlo con400, per evitare ambiguità fra intermediari), il valore ripulito dagli spazi ai bordi. Una riga che comincia con spazio (obs-fold, header ripreso sulla riga dopo) si rifiuta. - Host obbligatorio in HTTP/1.1.
- Lunghezza del corpo:
Content-Lengthdeve essere un intero non negativo;Content-LengtheTransfer-Encoding: chunkedinsieme sono rifiutati (400): è l'ambiguità su cui si basa il request smuggling. - Persistenza: in HTTP/1.1 la connessione resta aperta a meno di
Connection: close; in HTTP/1.0 si chiude a meno diConnection: keep-alive. L'headerConnectionè una lista separata da virgole (keep-alive, Upgrade):has_tokencerca una parola intera. - Target: si divide in percorso e query (al primo
?); solo il percorso si decodifica; si controlla..dopo la decodifica.
Codice
/* parse_http.c - parsing di una richiesta HTTP gia' ricevuta in memoria, con autotest.
* Non apre socket: serve a ragionare solo sul parsing (request line, header, URI, URL assoluto).
* Compilare e provare: gcc -Wall -Wextra -o parse_http parse_http.c && ./parse_http
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <ctype.h>
#undef NDEBUG /* gli assert degli autotest devono restare attivi anche con -DNDEBUG */
#include <assert.h>
#define MAX_HEADERS 32
#define MAX_LINE 1024 /* lunghezza massima di una riga (request line o header) */
#define PARSE_OK 0
#define PARSE_INCOMPLETE (-1) /* servono altri byte dal socket */
struct header {
char name[64];
char value[MAX_LINE];
};
struct request {
char method[16];
char target[MAX_LINE]; /* request-target cosi' come arriva */
char path[MAX_LINE]; /* parte prima di '?', gia' decodificata */
char query[MAX_LINE]; /* parte dopo '?', NON decodificata */
int major, minor;
struct header headers[MAX_HEADERS];
int nheaders;
long content_length; /* -1 se non c'e' */
int chunked; /* 1 se Transfer-Encoding: chunked */
int keep_alive; /* 1 se la connessione resta aperta dopo la risposta */
size_t head_len; /* byte occupati da request line + header + riga vuota */
};
/* ---------- 1. una riga alla volta ---------- */
/* Copia in out la prossima riga di buf[*pos..len) senza CRLF (o LF) e avanza *pos.
* Torna 1 se ha letto una riga intera, 0 se manca ancora il terminatore, -1 se la riga e' troppo lunga. */
static int next_line(const char *buf, size_t len, size_t *pos, char *out, size_t outsz)
{
size_t i = *pos;
while (i < len && buf[i] != '\n')
i++;
if (i == len)
return (len - *pos >= outsz) ? -1 : 0; /* senza LF: incompleta, o gia' troppo lunga */
size_t n = i - *pos;
if (n > 0 && buf[*pos + n - 1] == '\r')
n--; /* via il CR */
if (n >= outsz)
return -1;
memcpy(out, buf + *pos, n);
out[n] = '\0';
*pos = i + 1;
return 1;
}
/* ---------- 2. percent-decoding e sicurezza del percorso ---------- */
static int hexval(int c)
{
if (c >= '0' && c <= '9') return c - '0';
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
return -1;
}
/* "caff%C3%A8%20latte" -> "caffè latte". Torna -1 se un '%' non e' seguito da due cifre esadecimali
* o se il risultato contiene un byte NUL (%00: serve a ingannare i controlli sulle stringhe C). */
static int url_decode(const char *src, char *dst, size_t dsize)
{
size_t n = 0;
for (; *src; src++) {
int c = (unsigned char)*src;
if (c == '%') {
int h = hexval((unsigned char)src[1]);
int l = (h >= 0) ? hexval((unsigned char)src[2]) : -1; /* se src[1] e' '\0' non si guarda oltre */
if (h < 0 || l < 0)
return -1;
c = h * 16 + l;
src += 2;
if (c == 0)
return -1;
}
if (n + 1 >= dsize)
return -1;
dst[n++] = (char)c;
}
dst[n] = '\0';
return 0;
}
/* Vero se il percorso (gia' decodificato) non esce dalla radice: niente segmento "..". */
static int path_is_safe(const char *p)
{
if (p[0] != '/')
return 0;
for (const char *s = p; *s;) {
const char *e = strchr(s + 1, '/');
size_t len = e ? (size_t)(e - s) - 1 : strlen(s + 1); /* lunghezza del segmento dopo '/' */
if (len == 2 && s[1] == '.' && s[2] == '.')
return 0;
if (!e)
break;
s = e;
}
return 1;
}
/* ---------- 3. header ---------- */
static const char *header_get(const struct request *r, const char *name)
{
for (int i = 0; i < r->nheaders; i++)
if (strcasecmp(r->headers[i].name, name) == 0)
return r->headers[i].value;
return NULL;
}
/* Il valore di Connection e' una lista separata da virgole ("keep-alive, Upgrade"): cerca un token. */
static int has_token(const char *list, const char *token)
{
size_t k = strlen(token);
for (const char *p = list; *p;) {
while (*p == ' ' || *p == '\t' || *p == ',')
p++;
const char *q = p;
while (*q && *q != ',' && *q != ' ' && *q != '\t')
q++;
if ((size_t)(q - p) == k && strncasecmp(p, token, k) == 0)
return 1;
p = q;
}
return 0;
}
/* ---------- 4. la richiesta completa ---------- */
/* Torna PARSE_OK, PARSE_INCOMPLETE oppure il codice di stato HTTP da rispondere (400, 414, 431, 501, 505). */
static int parse_request(const char *buf, size_t len, struct request *r)
{
char line[MAX_LINE];
size_t pos = 0;
memset(r, 0, sizeof *r);
r->content_length = -1;
/* request line: METODO SP TARGET SP HTTP/x.y */
int rc = next_line(buf, len, &pos, line, sizeof line);
if (rc == 0) return PARSE_INCOMPLETE;
if (rc < 0) return 414; /* URI Too Long */
char *sp1 = strchr(line, ' ');
char *sp2 = sp1 ? strchr(sp1 + 1, ' ') : NULL;
if (sp1 == NULL || sp2 == NULL || strchr(sp2 + 1, ' ') != NULL)
return 400; /* servono esattamente due spazi */
*sp1 = *sp2 = '\0';
const char *method = line, *target = sp1 + 1, *version = sp2 + 1;
size_t ml = strlen(method);
if (ml == 0 || ml >= sizeof r->method || strlen(target) == 0)
return 400;
for (size_t i = 0; i < ml; i++)
if (!isupper((unsigned char)method[i]))
return 400; /* il metodo e' un token in maiuscolo */
if (sscanf(version, "HTTP/%1d.%1d", &r->major, &r->minor) != 2 || strlen(version) != 8)
return 400;
if (r->major != 1)
return 505; /* HTTP Version Not Supported */
strcpy(r->method, method);
strcpy(r->target, target);
/* request-target: path[?query], con percent-decoding solo sul path */
const char *q = strchr(target, '?');
char rawpath[MAX_LINE];
size_t plen = q ? (size_t)(q - target) : strlen(target);
memcpy(rawpath, target, plen);
rawpath[plen] = '\0';
if (q)
snprintf(r->query, sizeof r->query, "%s", q + 1);
if (target[0] == '/') {
if (url_decode(rawpath, r->path, sizeof r->path) < 0)
return 400;
} else {
snprintf(r->path, sizeof r->path, "%s", rawpath); /* forma assoluta o CONNECT: la gestisce il chiamante */
}
/* header: "Nome: valore" fino alla riga vuota */
for (;;) {
rc = next_line(buf, len, &pos, line, sizeof line);
if (rc == 0) return PARSE_INCOMPLETE;
if (rc < 0) return 431; /* Request Header Fields Too Large */
if (line[0] == '\0')
break; /* riga vuota: fine degli header */
if (line[0] == ' ' || line[0] == '\t')
return 400; /* line folding obsoleto: lo rifiutiamo */
char *colon = strchr(line, ':');
if (colon == NULL || colon == line || colon[-1] == ' ' || colon[-1] == '\t')
return 400; /* niente ':' o spazio prima dei due punti */
if (r->nheaders == MAX_HEADERS)
return 431;
*colon = '\0';
char *v = colon + 1;
while (*v == ' ' || *v == '\t')
v++;
size_t vl = strlen(v);
while (vl > 0 && (v[vl - 1] == ' ' || v[vl - 1] == '\t'))
v[--vl] = '\0';
if (strlen(line) >= sizeof r->headers[0].name)
return 431;
struct header *h = &r->headers[r->nheaders++];
strcpy(h->name, line);
strcpy(h->value, v);
}
r->head_len = pos;
/* regole di HTTP/1.1 */
const char *host = header_get(r, "Host");
if (r->minor >= 1 && r->major == 1 && host == NULL)
return 400; /* Host e' obbligatorio in HTTP/1.1 */
const char *te = header_get(r, "Transfer-Encoding");
const char *cl = header_get(r, "Content-Length");
if (te != NULL && has_token(te, "chunked"))
r->chunked = 1;
if (cl != NULL) {
char *end;
long v = strtol(cl, &end, 10);
if (end == cl || *end != '\0' || v < 0)
return 400; /* Content-Length non valido */
if (r->chunked)
return 400; /* TE e CL insieme: sospetto request smuggling */
r->content_length = v;
}
const char *conn = header_get(r, "Connection");
if (r->minor >= 1)
r->keep_alive = !(conn && has_token(conn, "close")); /* 1.1: persistente salvo "close" */
else
r->keep_alive = (conn && has_token(conn, "keep-alive")); /* 1.0: chiusa salvo "keep-alive" */
return PARSE_OK;
}
/* ---------- 5. URL assoluto (forma usata verso un proxy): http://host[:porta][/percorso] ---------- */
static int parse_url(const char *url, char *host, size_t hsize, int *port, char *path, size_t psize)
{
if (strncasecmp(url, "http://", 7) != 0)
return -1;
const char *h = url + 7;
const char *slash = strchr(h, '/');
size_t alen = slash ? (size_t)(slash - h) : strlen(h); /* "host" oppure "host:porta" */
if (alen == 0 || alen >= hsize)
return -1;
char auth[256];
if (alen >= sizeof auth)
return -1;
memcpy(auth, h, alen);
auth[alen] = '\0';
*port = 80;
char *colon = strrchr(auth, ':');
if (colon != NULL) {
char *end;
long p = strtol(colon + 1, &end, 10);
if (end == colon + 1 || *end != '\0' || p < 1 || p > 65535)
return -1;
*port = (int)p;
*colon = '\0';
}
snprintf(host, hsize, "%s", auth);
snprintf(path, psize, "%s", slash ? slash : "/"); /* senza percorso vale "/" */
return 0;
}
/* ---------- autotest ---------- */
static int parse_str(const char *s, struct request *r)
{
return parse_request(s, strlen(s), r);
}
int main(void)
{
struct request r;
char out[256];
/* GET valido, con spazi attorno ai valori e nomi in maiuscole diverse */
assert(parse_str("GET /dir/a%20b.html?x=1&y=2 HTTP/1.1\r\nHost: example.com\r\nUser-Agent: curl/8 \r\n"
"connection: keep-alive, Upgrade\r\n\r\n", &r) == PARSE_OK);
assert(strcmp(r.method, "GET") == 0 && strcmp(r.path, "/dir/a b.html") == 0 && strcmp(r.query, "x=1&y=2") == 0);
assert(r.major == 1 && r.minor == 1 && r.nheaders == 3);
assert(strcmp(header_get(&r, "user-agent"), "curl/8") == 0); /* nome case-insensitive, valore ripulito */
assert(r.keep_alive == 1 && r.content_length == -1);
/* POST con corpo: head_len dice dove finisce la testa e inizia il corpo */
const char *post = "POST /form HTTP/1.1\r\nHost: h\r\nContent-Length: 11\r\n\r\nnome=Mario!";
assert(parse_str(post, &r) == PARSE_OK);
assert(r.content_length == 11 && strncmp(post + r.head_len, "nome=Mario!", 11) == 0);
/* HTTP/1.0: chiusa di default; Connection: close in 1.1 */
assert(parse_str("GET / HTTP/1.0\r\n\r\n", &r) == PARSE_OK && r.keep_alive == 0);
assert(parse_str("GET / HTTP/1.0\r\nConnection: Keep-Alive\r\n\r\n", &r) == PARSE_OK && r.keep_alive == 1);
assert(parse_str("GET / HTTP/1.1\r\nHost: h\r\nConnection: close\r\n\r\n", &r) == PARSE_OK && r.keep_alive == 0);
/* LF semplice accettato */
assert(parse_str("GET / HTTP/1.1\nHost: h\n\n", &r) == PARSE_OK);
/* incompleta: manca la riga vuota o la fine della riga */
assert(parse_str("GET / HTTP/1.1\r\nHost: h\r\n", &r) == PARSE_INCOMPLETE);
assert(parse_str("GET / HTT", &r) == PARSE_INCOMPLETE);
/* errori */
assert(parse_str("GET /\r\n\r\n", &r) == 400); /* manca la versione (e' HTTP/0.9) */
assert(parse_str("GET / HTTP/1.1\r\nHost: h\r\n\r\n", &r) == 400); /* due spazi */
assert(parse_str("get / HTTP/1.1\r\nHost: h\r\n\r\n", &r) == 400); /* metodo minuscolo */
assert(parse_str("GET / HTTP/2.0\r\nHost: h\r\n\r\n", &r) == 505);
assert(parse_str("GET / HTTP/1.1\r\n\r\n", &r) == 400); /* 1.1 senza Host */
assert(parse_str("GET / HTTP/1.1\r\nHost h\r\n\r\n", &r) == 400); /* header senza ':' */
assert(parse_str("GET / HTTP/1.1\r\nHost : h\r\n\r\n", &r) == 400); /* spazio prima di ':' */
assert(parse_str("GET / HTTP/1.1\r\nHost: h\r\n continua\r\n\r\n", &r) == 400); /* obs-fold */
assert(parse_str("POST / HTTP/1.1\r\nHost: h\r\nContent-Length: abc\r\n\r\n", &r) == 400);
assert(parse_str("POST / HTTP/1.1\r\nHost: h\r\nContent-Length: 5\r\nTransfer-Encoding: chunked\r\n\r\n", &r) == 400);
assert(parse_str("GET /%zz HTTP/1.1\r\nHost: h\r\n\r\n", &r) == 400); /* percent-encoding rotto */
assert(parse_str("GET /a%00b HTTP/1.1\r\nHost: h\r\n\r\n", &r) == 400); /* byte NUL nascosto */
/* percorsi */
assert(url_decode("caff%C3%A8%20latte", out, sizeof out) == 0 && strcmp(out, "caff\xC3\xA8 latte") == 0);
assert(url_decode("100%", out, sizeof out) == -1);
assert(path_is_safe("/a/b.html") && path_is_safe("/") && path_is_safe("/a/..b/c") && path_is_safe("/a/b../c"));
assert(!path_is_safe("/../etc/passwd") && !path_is_safe("/a/../../x") && !path_is_safe("/a/..") && !path_is_safe("a/b"));
assert(parse_str("GET /%2e%2e/etc/passwd HTTP/1.1\r\nHost: h\r\n\r\n", &r) == PARSE_OK);
assert(strcmp(r.path, "/../etc/passwd") == 0 && !path_is_safe(r.path)); /* decodifica PRIMA di controllare */
/* URL assoluto (proxy) */
char host[128], path[256];
int port;
assert(parse_url("http://example.com/a/b?q=1", host, sizeof host, &port, path, sizeof path) == 0);
assert(strcmp(host, "example.com") == 0 && port == 80 && strcmp(path, "/a/b?q=1") == 0);
assert(parse_url("http://127.0.0.1:8080", host, sizeof host, &port, path, sizeof path) == 0);
assert(strcmp(host, "127.0.0.1") == 0 && port == 8080 && strcmp(path, "/") == 0);
assert(parse_url("https://x.org/", host, sizeof host, &port, path, sizeof path) == -1);
assert(parse_url("http://h:99999/", host, sizeof host, &port, path, sizeof path) == -1);
puts("parse_http: tutti i test passati");
return 0;
}Compilare e provare
$ gcc -Wall -Wextra -o parse_http parse_http.c
$ ./parse_http
parse_http: tutti i test passatiIl programma termina con un assert fallito (e il numero di riga) al primo test sbagliato. Per aggiungere un caso basta una riga:
assert(parse_str("GET /a%20b HTTP/1.1\r\nHost: h\r\n\r\n", &r) == PARSE_OK && strcmp(r.path, "/a b") == 0);Spiegazione dei punti chiave
next_line.
- Cerca il
\npartendo da*pos. Se non lo trova la riga è incompleta: restituisce0(oppure-1se già più lunga del buffer, perché non finirà mai). - Tolto il
\n, se il carattere prima è\rlo scarta: funziona con CRLF e con LF. - Copia la riga in
outterminata da'\0'e avanza*pos: chiamate successive scorrono il messaggio.
La request line.
strchrtrova il primo e il secondo spazio; un terzo spazio dopo la versione, oppure uno solo, dà400.- Il metodo deve essere fatto di lettere maiuscole (
isuppercon cast aunsigned char: mai passare uncharnegativo aisupper). sscanf(version, "HTTP/%1d.%1d", ...)constrlen(version) == 8: accetta soloHTTP/1.1,HTTP/2.0..., e una versione maggiore di 1 dà505.
Il target. q = strchr(target, '?'): prima del ? il percorso, dopo la query (non decodificata: i separatori & e = sarebbero ambigui dopo la decodifica). Un target che non inizia per / (URL assoluto, CONNECT host:porta, *) si lascia al chiamante.
url_decode.
- Per ogni
%servono due cifre esadecimali;hexvalrestituisce-1per un carattere non valido. L'espressioneh >= 0 ? hexval(src[2]) : -1evita di leggeresrc[2]sesrc[1]era già il terminatore. (unsigned char)*srcevita estensione di segno con byte oltre0x7F.%00si rifiuta: un byte nullo nel percorso fa sì che le funzioni su stringhe C si fermino a metà (/img/a.png%00.htmldiventa/img/a.png): il percorso che si controlla e quello che si usa possono non coincidere, e si aggirano controlli come quello sull'estensione.
path_is_safe. Cerca il segmento .. fra due / (o a inizio e fine). /a/..b/c e /a/b../c sono sicuri (..b non è un segmento di salita). Il test "/%2e%2e/etc/passwd": dopo url_decode il percorso è /../etc/passwd e va rifiutato; se il controllo fosse fatto sul testo grezzo, %2e%2e passerebbe. Prima si decodifica, poi si controlla.
Gli header.
- Il nome si copia in
name[64]; un nome più lungo dà431. I valori hanno un limite per riga (MAX_LINE) e gli header sono al massimoMAX_HEADERS, altrimenti431. colon[-1] == ' '(o tabulazione): spazio prima dei due punti,400.- I valori sono ripuliti con due cicli, uno per gli spazi iniziali e uno per quelli finali.
header_getusastrcasecmp: il nome non distingue maiuscole e minuscole.
Content-Length, chunked e keep_alive.
strtol(cl, &end, 10)con controlloend == cl || *end != '\0' || v < 0: un valore non numerico, vuoto o negativo dà400. Se è presente ancheTransfer-Encoding: chunkedsi rifiuta.req.head_len= byte occupati da request line, header e riga vuota: nel buffer, il corpo comincia abuf + head_len(il test dellaPOSTlo verifica).keep_alive:1.1->!(Connection contiene close);1.0->(Connection contiene keep-alive).
parse_url.
- Accetta solo lo schema
http://(strncasecmp: lo schema non distingue maiuscole). - L'autorità va dopo
//fino al primo/; se contiene:l'ultimo:separa la porta, convertita constrtole controllata (1-65535); senza porta vale 80; senza percorso vale/. - Non gestisce IPv6 fra parentesi (
http://[::1]:80/): si può estendere come inuri_demo.c(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 →).
Gli autotest. Ogni assert documenta una regola: richiesta valida con spazi attorno ai valori e nomi in maiuscole diverse; POST con corpo (e head_len); persistenza in 1.0 e 1.1; LF semplice; richieste incomplete; e poi gli errori (versione mancante, due spazi, metodo minuscolo, HTTP/2.0, Host assente, header senza :, spazio prima di :, line folding, Content-Length non numerico, Content-Length con Transfer-Encoding, %zz, %00).
Errori tipici
- Controllare il percorso prima di decodificarlo:
%2e%2eaggira il controllo. - Decodificare anche la query, o decodificare due volte (
%252ediventa%2e, poi.). - Passare a
isupper/isdigituncharcon segno senza il cast aunsigned char. - Leggere oltre la fine del buffer:
src[2]dopo un%seguito dal terminatore. - Ignorare che il messaggio può essere incompleto: il parser deve poter dire "servono altri byte" e il chiamante rileggere dal socket.
- Confrontare i nomi degli header con
strcmp. - Fidarsi di
Content-Lengthsenza controllare che sia un numero valido e non enorme. - Accettare
Content-LengtheTransfer-Encodinginsieme. - Usare
strcpysenza controllare le dimensioni: ogni copia in questo codice è preceduta da un controllo di lunghezza.
Varianti per esercitarsi
- Aggiungere il caso
Expect: 100-continueeConnection: Upgrade. - Gestire
HTTP/0.9(GET /senza versione): oggi dà400. - Convertire le funzioni in un parser incrementale che mantiene lo stato fra una
reade la successiva. - Usare le funzioni in un server: Esercizio - Server HTTP iterativo con GET, HEAD e codici di errore (sul modello della prova pratica) ha una versione ridotta, Esercizio - Proxy HTTP con filtro degli host (sul modello della prova pratica) usa
parse_url.
Versione ripasso
- Testo.
parse_request(buf, len, &req)-> metodo, target (percorso decodificato + query), versione, header,Content-Length,chunked,keep_alive; ritorno0ok,PARSE_INCOMPLETE(servono altri byte) o codice di stato (400,414,431,505). Inoltreurl_decode,path_is_safe,parse_url, e autotest. - Righe.
next_line: cerca\n, toglie un\rprecedente (CRLF e LF), copia terminata; ritorni:1riga,0incompleta,-1troppo lunga.head_len= byte fino alla riga vuota: il corpo parte dabuf + head_len. - Request line.
METODO SP TARGET SP HTTP/x.y, esattamente due spazi; metodo di maiuscole (isupper((unsigned char)c));sscanf("HTTP/%1d.%1d")+strlen == 8; versione maggiore di 1 ->505. - Target. Percorso prima del primo
?, query dopo (non decodificata); si decodifica solo il percorso. url_decode.%HHcon due cifre esadecimali (hexval), valori con(unsigned char),%00rifiutato,-1su errore; non leggeresrc[2]dopo il terminatore.path_is_safe. Nessun segmento..(/a/..b/cva bene). Prima si decodifica, poi si controlla:/%2e%2e/etc/passwd=/../etc/passwd-> rifiutato.- Header.
Nome: valore, niente spazio prima di:(400), obs-fold rifiutato, valore ripulito dagli spazi ai bordi, limiti (431),strcasecmp.Hostobbligatorio in 1.1 (400). - Corpo e connessione.
Content-Lengthconstrtol(numero >= 0, altrimenti400);Content-Length+Transfer-Encoding: chunked=400(smuggling).keep_alive: 1.1 = nonclose; 1.0 = hakeep-alive;Connectionè una lista:has_token. parse_url. Solohttp://; autorità fino al primo/; ultimo:= porta (1-65535, default 80); percorso default/.- Prova.
gcc -Wall -Wextra -o parse_http parse_http.c && ./parse_http->parse_http: tutti i test passati; unassertrotto indica la regola violata. - Codice essenziale (le funzioni centrali, senza commenti):
static int next_line(const char *buf, size_t len, size_t *pos, char *out, size_t outsz)
{
size_t i = *pos;
while (i < len && buf[i] != '\n')
i++;
if (i == len)
return (len - *pos >= outsz) ? -1 : 0;
size_t n = i - *pos;
if (n > 0 && buf[*pos + n - 1] == '\r')
n--;
if (n >= outsz)
return -1;
memcpy(out, buf + *pos, n);
out[n] = '\0';
*pos = i + 1;
return 1;
}
static int url_decode(const char *src, char *dst, size_t dsize)
{
size_t n = 0;
for (; *src; src++) {
int c = (unsigned char)*src;
if (c == '%') {
int h = hexval((unsigned char)src[1]);
int l = (h >= 0) ? hexval((unsigned char)src[2]) : -1;
if (h < 0 || l < 0)
return -1;
c = h * 16 + l;
src += 2;
if (c == 0)
return -1;
}
if (n + 1 >= dsize)
return -1;
dst[n++] = (char)c;
}
dst[n] = '\0';
return 0;
}
static int path_is_safe(const char *p)
{
if (p[0] != '/')
return 0;
for (const char *s = p; *s;) {
const char *e = strchr(s + 1, '/');
size_t len = e ? (size_t)(e - s) - 1 : strlen(s + 1);
if (len == 2 && s[1] == '.' && s[2] == '.')
return 0;
if (!e)
break;
s = e;
}
return 1;
}
static int parse_url(const char *url, char *host, size_t hsize, int *port, char *path, size_t psize)
{
if (strncasecmp(url, "http://", 7) != 0)
return -1;
const char *h = url + 7;
const char *slash = strchr(h, '/');
size_t alen = slash ? (size_t)(slash - h) : strlen(h);
if (alen == 0 || alen >= hsize)
return -1;
char auth[256];
if (alen >= sizeof auth)
return -1;
memcpy(auth, h, alen);
auth[alen] = '\0';
*port = 80;
char *colon = strrchr(auth, ':');
if (colon != NULL) {
char *end;
long p = strtol(colon + 1, &end, 10);
if (end == colon + 1 || *end != '\0' || p < 1 || p > 65535)
return -1;
*port = (int)p;
*colon = '\0';
}
snprintf(host, hsize, "%s", auth);
snprintf(path, psize, "%s", slash ? slash : "/");
return 0;
}static const char *header_get(const struct request *r, const char *name)
{
for (int i = 0; i < r->nheaders; i++)
if (strcasecmp(r->headers[i].name, name) == 0)
return r->headers[i].value;
return NULL;
}
static int has_token(const char *list, const char *token)
{
size_t k = strlen(token);
for (const char *p = list; *p;) {
while (*p == ' ' || *p == '\t' || *p == ',')
p++;
const char *q = p;
while (*q && *q != ',' && *q != ' ' && *q != '\t')
q++;
if ((size_t)(q - p) == k && strncasecmp(p, token, k) == 0)
return 1;
p = q;
}
return 0;
}- Errori tipici: controllo del percorso prima della decodifica; query decodificata o doppia decodifica;
isuppersucharcon segno; lettura oltre il buffer; messaggio incompleto ignorato;strcmpsui nomi;Content-Lengthnon validato;Content-Lengthe chunked accettati insieme;strcpysenza controlli.