Esercizio - Proxy HTTP con filtro degli host (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 proxy HTTP (forward proxy) che ascolta su una porta e inoltra le richieste dei client ai server di origine:
- il client scrive nella request line l'URI assoluto (
GET http://host[:porta]/percorso HTTP/1.1); il proxy lo scompone, si collega ahost:portae invia al server la richiesta in forma normale (GET /percorso HTTP/1.1con l'headerHostcorretto); - supportare
GET,HEADePOST(conContent-Length): il corpo dellaPOSTva inoltrato; - non inoltrare gli header hop-by-hop (
Connection,Proxy-Connection,Keep-Alive,Transfer-Encoding, ...), aggiungereViaeX-Forwarded-For, e dichiarareConnection: closeverso il server; - girare la risposta del server al client, tale e quale;
- rifiutare con
403 Forbiddenle richieste verso gli host vietati elencati sulla riga di comando;400se l'URI non è assoluto;405per i metodi non supportati;501perCONNECTe per corpichunked;502 Bad Gatewayse non riesce a collegarsi al server.
Teoria: DNS, proxy web, HTTP CONNECT e gateway applicativiUn programma risolve i nomi con getaddrinfo (file hosts e poi DNS); un messaggio DNS (RFC 1035) ha un header di 12 byte (ID, flag QR/RD/RA/TC e RCODE, quattro contatori), una domanda (QNAME a etichette con lunghezza, QTYPE, QCLASS) e record di risposta con nomi eventualmente compressi da puntatori, su UDP porta 53 con ripiego su TCP se il bit TC e' acceso; un web proxy e' un intermediario scelto dal client che riceve richieste con URI assoluto, le inoltra al server (togliendo gli header hop-by-hop, aggiungendo Via e X-Forwarded-For) e puo' memorizzare le risposte; le cache si organizzano in gerarchie e con funzioni hash coerenti, le CDN le replicano vicino agli utenti; il metodo CONNECT chiede al proxy un tunnel TCP verso host:porta e dopo la risposta 2xx il proxy inoltra i byte in entrambe le direzioni senza interpretarli (HTTPS, con restrizione delle porte); un gateway o reverse proxy e' un intermediario scelto dal server che traduce o inoltra le richieste ad altri sistemi (bilanciamento, terminazione TLS, FastCGI, API gateway).DNS, proxy web, HTTP CONNECT e gateway applicativi →, 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 →, Livello applicazione - HTTPIl livello applicazione è il più alto della pila: offre servizi all'utente con una connessione logica tra le due applicazioni e riceve servizi solo dal trasporto (DNS, HTTP, e-mail, FTP). Il Web (WWW, nato al CERN nel 1989) è un servizio client-server distribuito di pagine collegate da ipertesti; ogni pagina ha un URL protocollo://host:porta/percorso. HTTP: il client manda una richiesta, il server una risposta, su TCP (server sulla porta 80, client su una porta temporanea); senza stato. Messaggi di testo (riga di richiesta o di stato, intestazioni, riga vuota, corpo), metodi GET, POST, HEAD, PUT, DELETE, codici di stato 2xx-5xx. Una pagina con N oggetti incorporati richiede 2(N+1) RTT con connessioni non persistenti e (N+2) RTT con connessione persistente (trascurando la trasmissione). I cookie danno memoria al protocollo: Set-Cookie nella risposta, Cookie nelle richieste, file nel browser e base di dati nel sito. Un proxy (web cache) tiene le copie delle risposte recenti: meno carico sul server, meno traffico, meno ritardo.Livello applicazione - HTTP →.
Idea
Il proxy è due programmi in uno: è un server per il client (accetta la connessione) e un client per il server di origine (apre una connessione verso di lui). Per ogni richiesta:
client ──► proxy ──► server di origine
GET http://example.com:8081/p?q=1 HTTP/1.1 GET /p?q=1 HTTP/1.1
Host: wrong Host: example.com:8081
Connection: keep-alive ───► Accept: */*
Accept: */* Via: 1.1 mini-proxy
Proxy-Connection: keep-alive X-Forwarded-For: 10.0.0.7
Keep-Alive: 5 Connection: closePassi di handle:
- Legge la richiesta del client (request line e header) con il lettore a buffer, memorizzando gli header in due array (nomi e valori).
- Controlli:
CONNECT→501(ha un esercizio proprio); metodo diverso daGET,HEAD,POST→405; headerTransfer-Encoding→501(non si decodifica il chunked in ingresso). parse_url: dahttp://host[:porta]/percorsoricava host, porta (80 se manca) e percorso (/se manca). Se l'URI non è assoluto (GET /x) il client non sa di parlare con un proxy →400.- Filtro: confronta l'host con la lista dei vietati (
strcasecmp: i nomi di dominio non distinguono maiuscole) →403. connect_to: se il collegamento fallisce →502 Bad Gateway.- Costruisce la richiesta in forma normale:
METODO percorso HTTP/1.1, un nuovoHost, tutti gli header del client tranne quelli hop-by-hop e il vecchioHost, poiVia,X-Forwarded-ForeConnection: close. - Inoltra il corpo della
POST(Content-Lengthbyte) conrb_copy. - Gira la risposta: legge la status-line (per il log), la inoltra, poi i byte già nel buffer, poi copia tutto con
read/write_allfinché il server chiude.
Perché Connection: close verso il server: così la risposta finisce sempre con la chiusura (o con Content-Length/chunked) e il proxy non deve capire la struttura della risposta: gli basta copiare i byte. Il client ottiene la risposta com'è (con la sua codifica Content-Length o chunked), poi il proxy chiude anche verso di lui.
Codice
/* http_proxy.c - proxy HTTP (forward proxy) semplice: GET, HEAD e POST con Content-Length, con lista di host vietati.
*
* Il client scrive la richiesta con URL ASSOLUTO ("GET http://host/percorso HTTP/1.1"); il proxy si connette
* all'host, manda la richiesta in forma normale ("GET /percorso HTTP/1.1"), e gira la risposta al client.
*
* Compilare: gcc -Wall -Wextra -o http_proxy http_proxy.c
* Avviare: ./http_proxy 8888 [host_vietato ...]
* Provare: curl -x http://127.0.0.1:8888 http://example.com/ (curl -x usa il proxy)
* curl -x http://127.0.0.1:8888 -I http://example.com/ (HEAD)
* printf 'GET http://example.com/ HTTP/1.0\r\n\r\n' | nc 127.0.0.1 8888
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <errno.h>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#define MAX_LINE 4096
#define MAX_HEADERS 64
static char **blocked; /* host vietati (da argv) */
static int nblocked;
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;
}
/* ---------- lettore con buffer ---------- */
struct rbuf {
int fd;
char buf[4096];
size_t pos, len;
};
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 -2;
line[n++] = c;
}
}
/* Inoltra n byte dal lettore src al socket dst. */
static int rb_copy(struct rbuf *src, int dst, long n)
{
while (n > 0) {
if (rb_fill(src) < 0)
return -1;
size_t avail = src->len - src->pos;
size_t take = ((long)avail < n) ? avail : (size_t)n;
if (write_all(dst, src->buf + src->pos, take) < 0)
return -1;
src->pos += take;
n -= (long)take;
}
return 0;
}
/* ---------- URL assoluto ---------- */
static int parse_url(const char *url, char *host, size_t hsize, char *port, size_t psize, char *path, size_t pathsize)
{
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);
char auth[256];
if (alen == 0 || alen >= sizeof auth)
return -1;
memcpy(auth, h, alen);
auth[alen] = '\0';
char *colon = strrchr(auth, ':');
if (colon != NULL) {
*colon = '\0';
snprintf(port, psize, "%s", colon + 1);
if (atoi(port) < 1 || atoi(port) > 65535)
return -1;
} else {
snprintf(port, psize, "80");
}
snprintf(host, hsize, "%s", auth);
snprintf(path, pathsize, "%s", slash ? slash : "/");
return host[0] ? 0 : -1;
}
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 void send_error(int fd, int code, const char *reason)
{
char msg[512];
int blen = snprintf(msg, sizeof msg, "%d %s\n", code, reason);
char head[512];
int n = snprintf(head, sizeof head,
"HTTP/1.1 %d %s\r\nContent-Type: text/plain\r\nContent-Length: %d\r\nConnection: close\r\n\r\n",
code, reason, blen);
write_all(fd, head, (size_t)n);
write_all(fd, msg, (size_t)blen);
}
/* Header "hop-by-hop": riguardano solo la connessione fra due nodi e un proxy NON li inoltra (RFC 9110 7.6.1). */
static int is_hop_by_hop(const char *name)
{
static const char *hop[] = {"Connection", "Proxy-Connection", "Keep-Alive", "Proxy-Authenticate",
"Proxy-Authorization", "TE", "Trailer", "Transfer-Encoding", "Upgrade"};
for (size_t i = 0; i < sizeof hop / sizeof hop[0]; i++)
if (strcasecmp(name, hop[i]) == 0)
return 1;
return 0;
}
static void handle(int cfd, const char *peer)
{
struct rbuf cl = {.fd = cfd};
char line[MAX_LINE], method[16], target[MAX_LINE], ver[16];
/* ---- richiesta del client ---- */
int len = rb_line(&cl, line, sizeof line);
if (len < 0 || sscanf(line, "%15s %4095s %15s", method, target, ver) != 3 || strncmp(ver, "HTTP/1.", 7) != 0) {
send_error(cfd, 400, "Bad Request");
return;
}
static char names[MAX_HEADERS][64], values[MAX_HEADERS][1024];
int nh = 0;
long clen = 0;
for (;;) {
len = rb_line(&cl, line, sizeof line);
if (len < 0) {
send_error(cfd, 400, "Bad Request");
return;
}
if (len == 0)
break;
char *colon = strchr(line, ':');
if (colon == NULL || nh >= MAX_HEADERS) {
send_error(cfd, 400, "Bad Request");
return;
}
*colon = '\0';
const char *v = colon + 1;
while (*v == ' ' || *v == '\t')
v++;
snprintf(names[nh], sizeof names[0], "%s", line);
snprintf(values[nh], sizeof values[0], "%s", v);
if (strcasecmp(line, "Content-Length") == 0)
clen = atol(v);
if (strcasecmp(line, "Transfer-Encoding") == 0) {
send_error(cfd, 501, "Not Implemented"); /* corpo chunked dal client: non gestito */
return;
}
nh++;
}
if (strcmp(method, "CONNECT") == 0) {
send_error(cfd, 501, "Not Implemented"); /* i tunnel CONNECT sono nell'esercizio dedicato */
return;
}
if (strcmp(method, "GET") != 0 && strcmp(method, "HEAD") != 0 && strcmp(method, "POST") != 0) {
send_error(cfd, 405, "Method Not Allowed");
return;
}
char host[256], port[16], path[MAX_LINE];
if (parse_url(target, host, sizeof host, port, sizeof port, path, sizeof path) < 0) {
/* forma "origin" (/percorso): il client crede di parlare con un server, non con un proxy */
send_error(cfd, 400, "Bad Request: serve un URL assoluto http://...");
return;
}
for (int i = 0; i < nblocked; i++)
if (strcasecmp(host, blocked[i]) == 0) {
fprintf(stderr, "%s %s %s -> 403 (host vietato)\n", peer, method, target);
send_error(cfd, 403, "Forbidden");
return;
}
/* ---- connessione al server di origine ---- */
int sfd = connect_to(host, port);
if (sfd < 0) {
fprintf(stderr, "%s %s %s -> 502 (connessione a %s:%s fallita)\n", peer, method, target, host, port);
send_error(cfd, 502, "Bad Gateway");
return;
}
/* ---- richiesta inoltrata ---- */
char req[16384];
int n = snprintf(req, sizeof req, "%s %s HTTP/1.1\r\n", method, path); /* da URL assoluto a origin-form */
int is_default_port = strcmp(port, "80") == 0;
n += snprintf(req + n, sizeof req - (size_t)n, is_default_port ? "Host: %s\r\n" : "Host: %s:%s\r\n", host, port);
for (int i = 0; i < nh; i++) {
if (is_hop_by_hop(names[i]) || strcasecmp(names[i], "Host") == 0)
continue; /* Host rifatto sopra; hop-by-hop non inoltrati */
n += snprintf(req + n, sizeof req - (size_t)n, "%s: %s\r\n", names[i], values[i]);
if ((size_t)n >= sizeof req - 256) {
send_error(cfd, 431, "Request Header Fields Too Large");
close(sfd);
return;
}
}
n += snprintf(req + n, sizeof req - (size_t)n,
"Via: 1.1 mini-proxy\r\nX-Forwarded-For: %s\r\nConnection: close\r\n\r\n", peer);
if (write_all(sfd, req, (size_t)n) < 0 || (clen > 0 && rb_copy(&cl, sfd, clen) < 0)) {
send_error(cfd, 502, "Bad Gateway");
close(sfd);
return;
}
/* ---- risposta: si legge la status-line per il log, poi si gira tutto com'e' ---- */
struct rbuf up = {.fd = sfd};
len = rb_line(&up, line, sizeof line);
if (len < 0) {
send_error(cfd, 502, "Bad Gateway"); /* il server ha chiuso senza rispondere */
close(sfd);
return;
}
fprintf(stderr, "%s %s %s -> %s\n", peer, method, target, line);
write_all(cfd, line, strlen(line));
write_all(cfd, "\r\n", 2);
if (up.pos < up.len) /* byte gia' letti oltre la status-line */
write_all(cfd, up.buf + up.pos, up.len - up.pos);
char buf[8192];
ssize_t r;
while ((r = read(sfd, buf, sizeof buf)) > 0) /* il server chiude (Connection: close): la risposta e' finita */
if (write_all(cfd, buf, (size_t)r) < 0)
break;
close(sfd);
}
int main(int argc, char **argv)
{
if (argc < 2) {
fprintf(stderr, "uso: %s porta [host_vietato ...]\n", argv[0]);
return 1;
}
blocked = argv + 2;
nblocked = argc - 2;
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;
}
fprintf(stderr, "proxy in ascolto sulla porta %s\n", argv[1]);
for (;;) {
struct sockaddr_in cli;
socklen_t clen = sizeof cli;
int cfd = accept(lfd, (struct sockaddr *)&cli, &clen);
if (cfd < 0) {
if (errno == EINTR)
continue;
perror("accept");
break;
}
char peer[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &cli.sin_addr, peer, sizeof peer);
handle(cfd, peer);
close(cfd);
}
return 0;
}Compilare e provare
$ gcc -Wall -Wextra -o http_proxy http_proxy.c
$ (cd www && python3 -m http.server 8000 --bind 127.0.0.1) & # un server di origine
$ ./http_proxy 8888 & # il proxy
proxy in ascolto sulla porta 8888
$ curl -i -x http://127.0.0.1:8888 http://127.0.0.1:8000/index.html # curl -x usa il proxy
HTTP/1.0 200 OK
Server: SimpleHTTP/0.6 Python/3.12.10
Content-Type: text/html
Content-Length: 40
...
<html><body><h1>Ciao</h1></body></html>Il proxy stampa una riga per richiesta, con la status-line del server:
127.0.0.1 GET http://127.0.0.1:8000/index.html -> HTTP/1.0 200 OKPer vedere cosa riceve il server si può usare nc -l 8000 al posto di Python: la richiesta inoltrata è
$ curl -x http://127.0.0.1:8888 http://127.0.0.1:8000/p?q=1 -H 'Proxy-Connection: keep-alive'
(sul terminale di nc -l 8000)
GET /p?q=1 HTTP/1.1
Host: 127.0.0.1:8000
User-Agent: curl/8.5.0
Accept: */*
Via: 1.1 mini-proxy
X-Forwarded-For: 127.0.0.1
Connection: closeSi noti: forma normale (/p?q=1), Host con la porta (non è la 80), nessun Proxy-Connection, Via e X-Forwarded-For aggiunti. Altre prove:
$ ./http_proxy 8888 bad.example.com & # host vietato
$ curl -i -x http://127.0.0.1:8888 http://bad.example.com/ # 403 Forbidden
$ printf 'GET /x HTTP/1.1\r\nHost: x\r\n\r\n' | nc 127.0.0.1 8888 # forma origin: 400 "serve un URL assoluto"
$ curl -i -x http://127.0.0.1:8888 http://127.0.0.1:9/ # porta chiusa: 502 Bad Gateway
$ curl -x http://127.0.0.1:8888 -d "a=1" http://127.0.0.1:8000/ # POST: corpo inoltrato (Python risponde 501, ma il corpo è arrivato)
$ curl -I -x http://127.0.0.1:8888 http://127.0.0.1:8000/index.html # HEAD: solo headerPer il browser: impostare il proxy HTTP 127.0.0.1:8888 nelle impostazioni di rete (solo http://: per https:// il browser userebbe CONNECT, che qui dà 501: Esercizio - Proxy con tunnel CONNECT (sul modello della prova pratica)).
Spiegazione dei punti chiave
parse_url. strncasecmp(url, "http://", 7): lo schema non distingue maiuscole. Il tratto fra // e il primo / è l'autorità (host oppure host:porta); l'ultimo : separa la porta, convertita con atoi e controllata fra 1 e 65535; senza porta vale 80. Il percorso è tutto ciò che segue il primo / (query compresa), oppure /. Un URI senza http:// non è assoluto: -1.
Gli header hop-by-hop (is_hop_by_hop). Sono quelli che descrivono la connessione con il vicino, non il messaggio: Connection, Proxy-Connection (non standard), Keep-Alive, Proxy-Authenticate, Proxy-Authorization, TE, Trailer, Transfer-Encoding, Upgrade. Un proxy non li inoltra (RFC 9110 par. 7.6.1): il proxy ha due connessioni separate, con la propria persistenza. Gli altri header (end-to-end: Accept, User-Agent, Cookie, Content-Type, Content-Length...) passano. Il vecchio Host si sostituisce con quello dell'URI (nella richiesta al proxy poteva essere sbagliato o assente): Host: host se la porta è 80, Host: host:porta altrimenti.
Via e X-Forwarded-For. Via: 1.1 mini-proxy documenta che il messaggio è passato da un intermediario HTTP/1.1; serve anche a riconoscere i cicli fra proxy. X-Forwarded-For porta l'indirizzo del client: il server di origine vedrebbe solo quello del proxy.
Lettura e inoltro del corpo. Content-Length si legge dall'header (atol); rb_copy(&cl, sfd, clen) copia esattamente clen byte dal client al server, usando prima i byte già nel buffer (rbuf): una read diretta perderebbe quelli già letti insieme agli header. Non si accetta Transfer-Encoding in ingresso (501), perché occorrerebbe decodificare i chunk.
La risposta. rb_line(&up, ...) legge la status-line (che si stampa nel log e si inoltra con \r\n); poi si scrivono al client i byte che il lettore aveva già in buffer (up.buf + up.pos, up.len - up.pos); poi un ciclo read/write_all copia il resto finché il server chiude (Connection: close). I byte non vengono interpretati: il proxy non decodifica il corpo, e il client riceve Content-Length o chunked esattamente come il server li ha prodotti.
Errori verso il client. send_error produce risposte brevi con Content-Length e Connection: close: 400, 403, 405, 501, 502.
Limiti di questa versione.
- È iterativo: un server lento tiene occupato il proxy. Si rende concorrente con
forkcome in Esercizio - Server HTTP concorrente con fork e connessioni persistenti (sul modello della prova pratica). - Una connessione per richiesta (nessuna persistenza né verso il client né verso il server).
- Nessuna cache: si potrebbe memorizzare la risposta per URL e riusarla (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 →).
- Nessun controllo di accesso (chiunque può usarlo: un open proxy): in produzione serve un'autenticazione (
407,Proxy-Authorization). - Nessun timeout.
- Solo IPv4 per l'ascolto (l'uscita usa
getaddrinfo, quindi anche IPv6).
Errori tipici
- Inoltrare al server l'URI assoluto nella request line (
GET http://...): i server di origine si aspettano la forma normale (alcuni lo accettano, molti no). - Lasciare
Hostcom'era nella richiesta del client invece di impostarlo con l'host di destinazione. - Inoltrare gli header hop-by-hop (
Connection: keep-aliveverso un server che poi chiude,Proxy-Connection,Transfer-Encoding). - Usare una
readdiretta doporb_line: i byte già nel buffer del lettore si perdono (il corpo dellaPOSTo l'inizio della risposta). - Dimenticare di inoltrare i byte già nel buffer dopo la status-line della risposta.
- Interpretare il corpo della risposta (fermarsi a
Content-Length): non serve, basta copiare fino alla chiusura se si imponeConnection: close. - Non chiudere
sfdin ogni percorso di errore (descrittori che si accumulano). - Confondere forward proxy e reverse proxy: qui il client sa di usare il proxy e scrive URI assoluti.
Varianti per esercitarsi
- Aggiungere una cache in memoria per le risposte
200alleGET, con scadenza daCache-Control: max-agee rivalidazione conIf-None-Match. - Autenticazione del client con
407eProxy-Authenticate: Basic. - Rendere il proxy concorrente e gestire il chunked in ingresso.
- Trasformarlo in reverse proxy: indirizzo fisso del server di origine e nessun URI assoluto; bilanciamento fra due server.
- Unirlo al tunnel
CONNECT(Esercizio - Proxy con tunnel CONNECT (sul modello della prova pratica)).
Versione ripasso
- Testo. Forward proxy
./http_proxy porta [host_vietato ...]: richiesta con URI assoluto, inoltro in forma normale conHostcorretto,GET/HEAD/POST(corpo conContent-Length), niente hop-by-hop,ViaeX-Forwarded-For,Connection: closeverso il server, risposta girata tale e quale;403host vietato,400URI non assoluto,405,501(CONNECT, chunked in ingresso),502. - Flusso. Richiesta del client (lettore a buffer, header in due array) -> controlli (
CONNECT501; metodo405;Transfer-Encoding501) ->parse_url(http://host[:porta]/percorso: porta di default 80, percorso di default/; URI non assoluto ->400) -> filtrostrcasecmp->connect_to(fallisce ->502) -> richiesta in forma normale -> corpo conrb_copy-> risposta. - Richiesta inoltrata.
METODO /percorso HTTP/1.1;Host: host(porta 80) oHost: host:porta; header del client tranne hop-by-hop e vecchioHost;Via: 1.1 mini-proxy;X-Forwarded-For: <IP client>;Connection: close. - Hop-by-hop (non inoltrati).
Connection,Proxy-Connection,Keep-Alive,Proxy-Authenticate,Proxy-Authorization,TE,Trailer,Transfer-Encoding,Upgrade. Gli end-to-end passano. - Risposta.
len = rb_line(&up, line, sizeof line); /* status-line: log + inoltro */
write_all(cfd, line, strlen(line)); write_all(cfd, "\r\n", 2);
if (up.pos < up.len) write_all(cfd, up.buf + up.pos, up.len - up.pos); /* byte già nel buffer */
while ((r = read(sfd, buf, sizeof buf)) > 0) write_all(cfd, buf, r); /* fino alla chiusura */ Il corpo non si interpreta: Content-Length o chunked passano così come sono.
- Prove.
curl -i -x http://127.0.0.1:8888 http://127.0.0.1:8000/index.html;nc -l 8000mostra la richiesta inoltrata (forma normale,Host: 127.0.0.1:8000,Via,X-Forwarded-For);./http_proxy 8888 bad.example.com->403;GET /xal proxy ->400; porta chiusa ->502. - Limiti. Iterativo, nessuna cache, nessuna persistenza, nessuna autenticazione (open proxy), nessun timeout.
- Codice essenziale (le funzioni centrali, senza commenti):
static int parse_url(const char *url, char *host, size_t hsize, char *port, size_t psize, char *path, size_t pathsize)
{
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);
char auth[256];
if (alen == 0 || alen >= sizeof auth)
return -1;
memcpy(auth, h, alen);
auth[alen] = '\0';
char *colon = strrchr(auth, ':');
if (colon != NULL) {
*colon = '\0';
snprintf(port, psize, "%s", colon + 1);
if (atoi(port) < 1 || atoi(port) > 65535)
return -1;
} else {
snprintf(port, psize, "80");
}
snprintf(host, hsize, "%s", auth);
snprintf(path, pathsize, "%s", slash ? slash : "/");
return host[0] ? 0 : -1;
}
static int is_hop_by_hop(const char *name)
{
static const char *hop[] = {"Connection", "Proxy-Connection", "Keep-Alive", "Proxy-Authenticate",
"Proxy-Authorization", "TE", "Trailer", "Transfer-Encoding", "Upgrade"};
for (size_t i = 0; i < sizeof hop / sizeof hop[0]; i++)
if (strcasecmp(name, hop[i]) == 0)
return 1;
return 0;
}
static int rb_copy(struct rbuf *src, int dst, long n)
{
while (n > 0) {
if (rb_fill(src) < 0)
return -1;
size_t avail = src->len - src->pos;
size_t take = ((long)avail < n) ? avail : (size_t)n;
if (write_all(dst, src->buf + src->pos, take) < 0)
return -1;
src->pos += take;
n -= (long)take;
}
return 0;
}static int connect_to(const char *host, const char *port)
{
struct addrinfo hints, *res, *p;
memset(&hints, 0, sizeof hints);
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
if (getaddrinfo(host, port, &hints, &res) != 0)
return -1;
int fd = -1;
for (p = res; p != NULL; p = p->ai_next) {
fd = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
if (fd < 0)
continue;
if (connect(fd, p->ai_addr, p->ai_addrlen) == 0)
break;
close(fd);
fd = -1;
}
freeaddrinfo(res);
return fd;
}- Errori tipici: URI assoluto verso il server;
Hostnon riscritto; hop-by-hop inoltrati;readdiretta doporb_line(byte persi); byte già nel buffer non inoltrati;sfdnon chiuso negli errori; forward e reverse proxy confusi.