Salta al contenuto
Note per Studenti Esercizio - Proxy HTTP con filtro degli host (sul modello della prova pratica)

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:

  1. il client scrive nella request line l'URI assoluto (GET http://host[:porta]/percorso HTTP/1.1); il proxy lo scompone, si collega a host:porta e invia al server la richiesta in forma normale (GET /percorso HTTP/1.1 con l'header Host corretto);
  2. supportare GET, HEAD e POST (con Content-Length): il corpo della POST va inoltrato;
  3. non inoltrare gli header hop-by-hop (Connection, Proxy-Connection, Keep-Alive, Transfer-Encoding, ...), aggiungere Via e X-Forwarded-For, e dichiarare Connection: close verso il server;
  4. girare la risposta del server al client, tale e quale;
  5. rifiutare con 403 Forbidden le richieste verso gli host vietati elencati sulla riga di comando; 400 se l'URI non è assoluto; 405 per i metodi non supportati; 501 per CONNECT e per corpi chunked; 502 Bad Gateway se 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: close

Passi di handle:

  1. Legge la richiesta del client (request line e header) con il lettore a buffer, memorizzando gli header in due array (nomi e valori).
  2. Controlli: CONNECT → 501 (ha un esercizio proprio); metodo diverso da GET, HEAD, POST → 405; header Transfer-Encoding → 501 (non si decodifica il chunked in ingresso).
  3. parse_url: da http://host[:porta]/percorso ricava 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.
  4. Filtro: confronta l'host con la lista dei vietati (strcasecmp: i nomi di dominio non distinguono maiuscole) → 403.
  5. connect_to: se il collegamento fallisce → 502 Bad Gateway.
  6. Costruisce la richiesta in forma normale: METODO percorso HTTP/1.1, un nuovo Host, tutti gli header del client tranne quelli hop-by-hop e il vecchio Host, poi Via, X-Forwarded-For e Connection: close.
  7. Inoltra il corpo della POST (Content-Length byte) con rb_copy.
  8. Gira la risposta: legge la status-line (per il log), la inoltra, poi i byte già nel buffer, poi copia tutto con read/write_all finché 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

c
/* 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 OK

Per 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: close

Si 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 header

Per 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.

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 Host com'era nella richiesta del client invece di impostarlo con l'host di destinazione.
  • Inoltrare gli header hop-by-hop (Connection: keep-alive verso un server che poi chiude, Proxy-Connection, Transfer-Encoding).
  • Usare una read diretta dopo rb_line: i byte già nel buffer del lettore si perdono (il corpo della POST o 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 impone Connection: close.
  • Non chiudere sfd in 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 200 alle GET, con scadenza da Cache-Control: max-age e rivalidazione con If-None-Match.
  • Autenticazione del client con 407 e Proxy-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 con Host corretto, GET/HEAD/POST (corpo con Content-Length), niente hop-by-hop, Via e X-Forwarded-For, Connection: close verso il server, risposta girata tale e quale; 403 host vietato, 400 URI non assoluto, 405, 501 (CONNECT, chunked in ingresso), 502.
  • Flusso. Richiesta del client (lettore a buffer, header in due array) -> controlli (CONNECT 501; metodo 405; Transfer-Encoding 501) -> parse_url (http://host[:porta]/percorso: porta di default 80, percorso di default /; URI non assoluto -> 400) -> filtro strcasecmp -> connect_to (fallisce -> 502) -> richiesta in forma normale -> corpo con rb_copy -> risposta.
  • Richiesta inoltrata. METODO /percorso HTTP/1.1; Host: host (porta 80) o Host: host:porta; header del client tranne hop-by-hop e vecchio Host; 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.
c
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 8000 mostra la richiesta inoltrata (forma normale, Host: 127.0.0.1:8000, Via, X-Forwarded-For); ./http_proxy 8888 bad.example.com -> 403; GET /x al proxy -> 400; porta chiusa -> 502.
  • Limiti. Iterativo, nessuna cache, nessuna persistenza, nessuna autenticazione (open proxy), nessun timeout.
  • Codice essenziale (le funzioni centrali, senza commenti):
c
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;
}
c
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; Host non riscritto; hop-by-hop inoltrati; read diretta dopo rb_line (byte persi); byte già nel buffer non inoltrati; sfd non chiuso negli errori; forward e reverse proxy confusi.

Esercizi su questo argomento

Teoria collegata