Salta al contenuto
Note per Studenti Esercizio - Client WebSocket con handshake, SHA-1 e Base64 (sul modello della prova pratica)

Esercizio - Client WebSocket con handshake, SHA-1 e Base64 (sul modello della prova pratica)

In questa pagina 7

Testo (sul modello della prova pratica di Reti di Calcolatori, Ing. Informatica UniPD).

Scrivere in C, senza librerie esterne, un client WebSocket (RFC 6455, solo ws://):

  1. aprire la connessione TCP e fare l'handshake: richiesta GET con Upgrade: websocket, Connection: Upgrade, Sec-WebSocket-Version: 13 e Sec-WebSocket-Key (16 byte casuali in Base64); controllare che la risposta sia 101 e che Sec-WebSocket-Accept valga Base64(SHA-1(chiave + GUID));
  2. implementare SHA-1 e Base64 e verificarli con i valori della RFC (esempio: chiave dGhlIHNhbXBsZSBub25jZQ== → s3pPLMBiTxaQ9kYGzzhZRbK+xOo=);
  3. inviare un messaggio di testo in un frame mascherato, con la lunghezza codificata correttamente (7, 16 o 64 bit);
  4. leggere i frame del server (non mascherati), gestire ping (rispondere con pong), close e messaggi frammentati; stampare il messaggio ricevuto;
  5. chiudere in modo ordinato con un frame close (codice 1000) e attendere il close del server.

Teoria: WebSocket, QUIC e HTTP-3HTTP e' richiesta e risposta, quindi poco adatto a notifiche e dati in tempo reale (polling, long polling, SSE); WebSocket (RFC 6455) apre con un handshake HTTP/1.1 Upgrade (Sec-WebSocket-Key a 16 byte casuali in Base64, risposta 101 con Sec-WebSocket-Accept = Base64 di SHA-1 di chiave piu' GUID fisso) un canale bidirezionale persistente sulla stessa connessione TCP, con frame di 2-14 byte di intestazione (FIN, opcode, MASK, lunghezza su 7, 16 o 64 bit, chiave di mascheramento obbligatoria dal client al server) e messaggi di testo, binari, close, ping e pong. QUIC (RFC 9000) e' un protocollo di trasporto sopra UDP con TLS 1.3 integrato, flussi indipendenti (niente head-of-line blocking fra flussi), connection ID che permettono di cambiare rete, handshake in 1 RTT e ripresa in 0 RTT; HTTP/3 (RFC 9114) mappa HTTP su QUIC con un flusso per richiesta e QPACK per gli header, ed e' annunciato con Alt-Svc.WebSocket, QUIC e HTTP-3 →, 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 →, 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 →, Funzioni hash e crittografia simmetricaLa crittografia trasforma un messaggio in chiaro (plaintext) in un testo cifrato (ciphertext) con una chiave: C = E_ke(P), P = D_kd(C); può dare riservatezza, integrità e autenticazione. Attacchi: solo testo cifrato, testo in chiaro noto, testo in chiaro scelto, forza bruta. Funzione hash: mappa dati di qualsiasi lunghezza in un digest di lunghezza fissa; one-way (dato h è difficile trovare m con hash(m) = h) e resistente alle collisioni; famiglie MD (MD5 rotto per le collisioni nel 2004) e SHA (SHA-0 e SHA-1 rotti, SHA-2 il più usato, SHA-3); costruzione di Merkle-Damgård; usi: integrità e password. Crittografia simmetrica: stessa chiave segreta per cifrare e decifrare; Cesare (E_n(x) = x + n mod 26), Vigenère, Enigma; DES (blocchi da 64 bit, chiave da 56) e AES (blocchi da 128 bit, chiavi da 128, 192, 256); modi ECB (insicuro), CBC, CFB, OFB, CTR. Scambio della chiave con Diffie-Hellman: K = g^(xy) mod p. MAC e HMAC: autenticazione con chiave condivisa.Funzioni hash e crittografia simmetrica →.


Idea

WebSocket comincia come una richiesta HTTP/1.1 e poi cambia protocollo. Il programma ha quattro parti.

1. Primitive. sha1 (FIPS 180-4) e base64_encode (RFC 4648), con un autotest (./ws_client --test) sui vettori noti: SHA-1("abc") = a9993e36..., Man → TWFu, Ma → TWE=, M → TQ==, e l'esempio dell'Accept della RFC 6455.

2. Handshake. Il client genera la chiave (16 byte da /dev/urandom in Base64: 24 caratteri), invia la richiesta, legge la risposta riga per riga (un byte alla volta: non deve leggere neppure un byte dei frame che seguono), controlla 101, Upgrade: websocket, Connection: Upgrade e il valore di Sec-WebSocket-Accept, ricalcolato localmente con ws_accept_value.

3. Frame in uscita (ws_build_frame): primo byte FIN | opcode; secondo byte MASK | lunghezza; se la lunghezza è 126 o più, due o otto byte in big endian; chiave di mascheramento di 4 byte; payload in XOR con la chiave.

4. Frame in ingresso (ws_read_frame) e ciclo dei messaggi: ping → pong, close → chiusura, testo/binario/continuazione → accumulo fino a FIN.

Codice

c
/* ws_client.c - client WebSocket (RFC 6455) scritto a mano: handshake HTTP Upgrade, frame mascherati, ping/pong, close.
 * Contiene SHA-1 e Base64 (servono per Sec-WebSocket-Accept) e un autotest sui valori della RFC.
 *
 * Compilare:  gcc -Wall -Wextra -o ws_client ws_client.c
 * Autotest:   ./ws_client --test
 * Usare:      ./ws_client host porta percorso "messaggio"         es. ./ws_client 127.0.0.1 9001 /echo "ciao"
 * Solo ws:// (senza TLS). Un server di prova: vedi il testo dell'esercizio.
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <stdint.h>
#include <errno.h>
#undef NDEBUG                      /* gli assert degli autotest devono restare attivi anche con -DNDEBUG */
#include <assert.h>
#include <time.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>

#define WS_GUID "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"   /* costante fissata dalla RFC 6455 par. 1.3 */
#define MAX_PAYLOAD 65536

/* ---------- SHA-1 (FIPS 180-4) ---------- */
static uint32_t rol(uint32_t x, int n)
{
    return (x << n) | (x >> (32 - n));
}

static void sha1_block(uint32_t h[5], const uint8_t *p)
{
    uint32_t w[80];
    for (int i = 0; i < 16; i++)                   /* le prime 16 parole: 64 byte letti in big endian */
        w[i] = (uint32_t)p[4 * i] << 24 | (uint32_t)p[4 * i + 1] << 16 | (uint32_t)p[4 * i + 2] << 8 | p[4 * i + 3];
    for (int i = 16; i < 80; i++)
        w[i] = rol(w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16], 1);
    uint32_t a = h[0], b = h[1], c = h[2], d = h[3], e = h[4];
    for (int i = 0; i < 80; i++) {
        uint32_t f, k;
        if (i < 20)      { f = (b & c) | (~b & d);          k = 0x5A827999; }
        else if (i < 40) { f = b ^ c ^ d;                   k = 0x6ED9EBA1; }
        else if (i < 60) { f = (b & c) | (b & d) | (c & d); k = 0x8F1BBCDC; }
        else             { f = b ^ c ^ d;                   k = 0xCA62C1D6; }
        uint32_t t = rol(a, 5) + f + e + k + w[i];
        e = d; d = c; c = rol(b, 30); b = a; a = t;
    }
    h[0] += a; h[1] += b; h[2] += c; h[3] += d; h[4] += e;
}

static void sha1(const uint8_t *msg, size_t len, uint8_t out[20])
{
    uint32_t h[5] = {0x67452301, 0xEFCDAB89, 0x98BADCFE, 0x10325476, 0xC3D2E1F0};
    size_t full = len / 64;
    for (size_t i = 0; i < full; i++)
        sha1_block(h, msg + 64 * i);
    /* ultimo blocco (o due): resto + byte 0x80 + zeri + lunghezza in bit su 8 byte */
    uint8_t tail[128];
    size_t rem = len - full * 64;
    memcpy(tail, msg + full * 64, rem);
    tail[rem] = 0x80;
    size_t tlen = (rem < 56) ? 64 : 128;
    memset(tail + rem + 1, 0, tlen - rem - 1);
    uint64_t bits = (uint64_t)len * 8;
    for (int i = 0; i < 8; i++)
        tail[tlen - 1 - i] = (uint8_t)(bits >> (8 * i));
    sha1_block(h, tail);
    if (tlen == 128)
        sha1_block(h, tail + 64);
    for (int i = 0; i < 5; i++) {
        out[4 * i] = (uint8_t)(h[i] >> 24);
        out[4 * i + 1] = (uint8_t)(h[i] >> 16);
        out[4 * i + 2] = (uint8_t)(h[i] >> 8);
        out[4 * i + 3] = (uint8_t)h[i];
    }
}

/* ---------- Base64 (RFC 4648 par. 4): 3 byte -> 4 caratteri da 6 bit, '=' di riempimento ---------- */
static void base64_encode(const uint8_t *in, size_t n, char *out)
{
    static const char T[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
    size_t o = 0;
    for (size_t i = 0; i < n; i += 3) {
        uint32_t v = (uint32_t)in[i] << 16 | (i + 1 < n ? (uint32_t)in[i + 1] << 8 : 0) | (i + 2 < n ? in[i + 2] : 0);
        out[o++] = T[(v >> 18) & 63];
        out[o++] = T[(v >> 12) & 63];
        out[o++] = (i + 1 < n) ? T[(v >> 6) & 63] : '=';
        out[o++] = (i + 2 < n) ? T[v & 63] : '=';
    }
    out[o] = '\0';
}

/* Sec-WebSocket-Accept = base64( sha1( chiave_del_client + GUID ) ) */
static void ws_accept_value(const char *key, char *out /* >= 29 byte */)
{
    char cat[128];
    int n = snprintf(cat, sizeof cat, "%s%s", key, WS_GUID);
    uint8_t digest[20];
    sha1((const uint8_t *)cat, (size_t)n, digest);
    base64_encode(digest, 20, out);
}

/* ---------- frame (RFC 6455 par. 5.2) ----------
 *   byte 0:  FIN(1) RSV(3) opcode(4)
 *   byte 1:  MASK(1) lunghezza(7)    <126: la lunghezza; 126: seguono 2 byte; 127: seguono 8 byte (big endian)
 *   [mask key 4 byte se MASK=1]  payload (messo in XOR con la chiave, byte i con key[i % 4])
 * I frame DAL client AL server devono essere mascherati; quelli dal server no. */
static size_t ws_build_frame(uint8_t *out, int opcode, const uint8_t *payload, size_t len, const uint8_t *maskkey)
{
    size_t o = 0;
    out[o++] = (uint8_t)(0x80 | opcode);           /* FIN = 1: messaggio in un solo frame */
    uint8_t m = maskkey ? 0x80 : 0;
    if (len < 126) {
        out[o++] = m | (uint8_t)len;
    } else if (len <= 0xFFFF) {
        out[o++] = m | 126;
        out[o++] = (uint8_t)(len >> 8);
        out[o++] = (uint8_t)len;
    } else {
        out[o++] = m | 127;
        for (int i = 7; i >= 0; i--)
            out[o++] = (uint8_t)((uint64_t)len >> (8 * i));
    }
    if (maskkey) {
        memcpy(out + o, maskkey, 4);
        o += 4;
        for (size_t i = 0; i < len; i++)
            out[o + i] = payload[i] ^ maskkey[i % 4];
    } else {
        memcpy(out + o, payload, len);
    }
    return o + len;
}

static int write_all(int fd, const void *buf, size_t n)
{
    const char *p = buf;
    while (n > 0) {
        ssize_t w = write(fd, p, n);
        if (w < 0) {
            if (errno == EINTR)
                continue;
            return -1;
        }
        p += w;
        n -= (size_t)w;
    }
    return 0;
}

static int read_exact(int fd, void *buf, size_t n)
{
    char *p = buf;
    while (n > 0) {
        ssize_t r = read(fd, p, n);
        if (r < 0 && errno == EINTR)
            continue;
        if (r <= 0)
            return -1;
        p += r;
        n -= (size_t)r;
    }
    return 0;
}

static void random_bytes(uint8_t *b, size_t n)
{
    FILE *f = fopen("/dev/urandom", "rb");
    if (f != NULL && fread(b, 1, n, f) == n) {
        fclose(f);
        return;
    }
    if (f != NULL)
        fclose(f);
    srand((unsigned)time(NULL));                   /* ripiego (non crittografico) */
    for (size_t i = 0; i < n; i++)
        b[i] = (uint8_t)rand();
}

/* Manda un frame (mascherato, con chiave casuale nuova a ogni frame). */
static int ws_send(int fd, int opcode, const uint8_t *payload, size_t len)
{
    static uint8_t frame[MAX_PAYLOAD + 16];
    if (len > MAX_PAYLOAD)
        return -1;
    uint8_t key[4];
    random_bytes(key, 4);
    size_t n = ws_build_frame(frame, opcode, payload, len, key);
    return write_all(fd, frame, n);
}

/* Legge UN frame. Torna 0 se ok, -1 su errore. */
static int ws_read_frame(int fd, int *opcode, int *fin, uint8_t *payload, size_t cap, size_t *len)
{
    uint8_t h[2];
    if (read_exact(fd, h, 2) < 0)
        return -1;
    *fin = (h[0] & 0x80) != 0;
    *opcode = h[0] & 0x0F;
    int masked = (h[1] & 0x80) != 0;
    uint64_t plen = h[1] & 0x7F;
    if (plen == 126) {
        uint8_t e[2];
        if (read_exact(fd, e, 2) < 0)
            return -1;
        plen = (uint64_t)e[0] << 8 | e[1];
    } else if (plen == 127) {
        uint8_t e[8];
        if (read_exact(fd, e, 8) < 0)
            return -1;
        plen = 0;
        for (int i = 0; i < 8; i++)
            plen = plen << 8 | e[i];
    }
    uint8_t mask[4] = {0, 0, 0, 0};
    if (masked && read_exact(fd, mask, 4) < 0)
        return -1;
    if (plen > cap)
        return -1;                                 /* troppo grande per il nostro buffer (close 1009) */
    if (read_exact(fd, payload, (size_t)plen) < 0)
        return -1;
    if (masked)
        for (size_t i = 0; i < plen; i++)
            payload[i] ^= mask[i % 4];
    *len = (size_t)plen;
    return 0;
}

/* ---------- 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;
}

/* Vero se hay contiene needle, senza distinguere maiuscole e minuscole. */
static int ci_contains(const char *hay, const char *needle)
{
    size_t k = strlen(needle);
    for (; *hay; hay++)
        if (strncasecmp(hay, needle, k) == 0)
            return 1;
    return 0;
}

static int read_line(int fd, char *line, size_t max)   /* un byte alla volta: non si legge oltre gli header */
{
    size_t n = 0;
    char c;
    while (n + 1 < max) {
        if (read(fd, &c, 1) <= 0)
            return -1;
        if (c == '\n') {
            if (n > 0 && line[n - 1] == '\r')
                n--;
            line[n] = '\0';
            return (int)n;
        }
        line[n++] = c;
    }
    return -1;
}

static int selftest(void)
{
    uint8_t d[20];
    char b64[64];
    sha1((const uint8_t *)"abc", 3, d);                              /* vettore FIPS 180: a9993e36... */
    assert(d[0] == 0xa9 && d[1] == 0x99 && d[2] == 0x3e && d[3] == 0x36 && d[19] == 0x9d);
    base64_encode((const uint8_t *)"Man", 3, b64);   assert(strcmp(b64, "TWFu") == 0);     /* RFC 4648 par. 10 */
    base64_encode((const uint8_t *)"Ma", 2, b64);    assert(strcmp(b64, "TWE=") == 0);
    base64_encode((const uint8_t *)"M", 1, b64);     assert(strcmp(b64, "TQ==") == 0);
    base64_encode((const uint8_t *)"foobar", 6, b64); assert(strcmp(b64, "Zm9vYmFy") == 0);
    ws_accept_value("dGhlIHNhbXBsZSBub25jZQ==", b64);                /* esempio di RFC 6455 par. 1.3 */
    assert(strcmp(b64, "s3pPLMBiTxaQ9kYGzzhZRbK+xOo=") == 0);
    uint8_t f[32];
    size_t n = ws_build_frame(f, 1, (const uint8_t *)"Hello", 5, NULL);   /* RFC 6455 par. 5.7: frame non mascherato */
    assert(n == 7 && f[0] == 0x81 && f[1] == 0x05 && memcmp(f + 2, "Hello", 5) == 0);
    const uint8_t key[4] = {0x37, 0xfa, 0x21, 0x3d};
    n = ws_build_frame(f, 1, (const uint8_t *)"Hello", 5, key);          /* ... e mascherato */
    const uint8_t exp[] = {0x81, 0x85, 0x37, 0xfa, 0x21, 0x3d, 0x7f, 0x9f, 0x4d, 0x51, 0x58};
    assert(n == sizeof exp && memcmp(f, exp, n) == 0);
    static uint8_t big[300];
    memset(big, 'x', sizeof big);
    static uint8_t bf[400];
    n = ws_build_frame(bf, 2, big, 300, NULL);                           /* 300 byte: lunghezza su 2 byte (126) */
    assert(n == 304 && bf[1] == 126 && bf[2] == 0x01 && bf[3] == 0x2C);
    puts("ws_client: autotest superato");
    return 0;
}

int main(int argc, char **argv)
{
    if (argc == 2 && strcmp(argv[1], "--test") == 0)
        return selftest();
    if (argc != 5) {
        fprintf(stderr, "uso: %s host porta percorso messaggio   (oppure --test)\n", argv[0]);
        return 1;
    }
    const char *host = argv[1], *port = argv[2], *path = argv[3], *msg = argv[4];
    int fd = connect_to(host, port);
    if (fd < 0) {
        fprintf(stderr, "connessione fallita\n");
        return 1;
    }

    /* ---- 1) handshake: una normale richiesta HTTP/1.1 GET con Upgrade ---- */
    uint8_t raw[16];
    char key[32];
    random_bytes(raw, sizeof raw);
    base64_encode(raw, sizeof raw, key);           /* 16 byte casuali in base64: 24 caratteri */
    char req[1024];
    int n = snprintf(req, sizeof req,
                     "GET %s HTTP/1.1\r\n"
                     "Host: %s:%s\r\n"
                     "Upgrade: websocket\r\n"
                     "Connection: Upgrade\r\n"
                     "Sec-WebSocket-Key: %s\r\n"
                     "Sec-WebSocket-Version: 13\r\n"
                     "\r\n",
                     path, host, port, key);
    if (write_all(fd, req, (size_t)n) < 0) {
        perror("write");
        return 1;
    }

    char line[1024], expected[32];
    ws_accept_value(key, expected);
    if (read_line(fd, line, sizeof line) < 0 || strncmp(line, "HTTP/1.1 101", 12) != 0) {
        fprintf(stderr, "handshake rifiutato: '%s'\n", line);   /* es. 400, 404, 426 Upgrade Required */
        return 1;
    }
    int ok_upgrade = 0, ok_conn = 0, ok_accept = 0;
    for (;;) {
        int len = read_line(fd, 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++;
        if (strcasecmp(line, "Upgrade") == 0 && strcasecmp(v, "websocket") == 0)
            ok_upgrade = 1;
        else if (strcasecmp(line, "Connection") == 0 && ci_contains(v, "upgrade"))
            ok_conn = 1;
        else if (strcasecmp(line, "Sec-WebSocket-Accept") == 0 && strcmp(v, expected) == 0)
            ok_accept = 1;                         /* prova che il server ha capito WebSocket (non e' una cache) */
    }
    if (!(ok_upgrade && ok_conn && ok_accept)) {
        fprintf(stderr, "handshake non valido (Upgrade=%d Connection=%d Accept=%d)\n", ok_upgrade, ok_conn, ok_accept);
        return 1;
    }
    printf("handshake completato, Sec-WebSocket-Accept = %s\n", expected);

    /* ---- 2) un messaggio di testo ---- */
    if (ws_send(fd, 0x1, (const uint8_t *)msg, strlen(msg)) < 0) {
        perror("send");
        return 1;
    }

    /* ---- 3) si ricevono frame fino alla risposta; ping -> pong; close -> fine ---- */
    static uint8_t payload[MAX_PAYLOAD];
    static uint8_t message[MAX_PAYLOAD];
    size_t mlen = 0;
    int got_reply = 0;
    while (!got_reply) {
        int op, fin;
        size_t len;
        if (ws_read_frame(fd, &op, &fin, payload, sizeof payload, &len) < 0) {
            fprintf(stderr, "connessione persa\n");
            return 1;
        }
        if (op == 0x9) {                           /* ping: si risponde con pong, stesso payload */
            ws_send(fd, 0xA, payload, len);
        } else if (op == 0xA) {
            /* pong non richiesto: ignorato */
        } else if (op == 0x8) {                    /* il server chiude per primo */
            ws_send(fd, 0x8, payload, len >= 2 ? 2 : 0);
            printf("il server ha chiuso la connessione\n");
            close(fd);
            return 0;
        } else if (op == 0x1 || op == 0x2 || op == 0x0) {   /* testo, binario, continuazione */
            if (mlen + len > sizeof message)
                return 1;
            memcpy(message + mlen, payload, len);
            mlen += len;
            if (fin)
                got_reply = 1;                     /* FIN = 1: messaggio completo (puo' essere in piu' frame) */
        }
    }
    printf("ricevuto (%zu byte): %.*s\n", mlen, (int)mlen, message);

    /* ---- 4) chiusura ordinata: Close con codice 1000, poi si attende il Close del server ---- */
    uint8_t code[2] = {0x03, 0xE8};                /* 1000 = chiusura normale, in big endian */
    ws_send(fd, 0x8, code, 2);
    for (;;) {
        int op, fin;
        size_t len;
        if (ws_read_frame(fd, &op, &fin, payload, sizeof payload, &len) < 0 || op == 0x8)
            break;
    }
    close(fd);
    printf("connessione chiusa\n");
    return 0;
}

Server di prova

Per provare il client senza internet serve un server WebSocket: questo piccolo server Python (solo libreria standard) fa l'handshake, risponde con eco: <messaggio> a ogni testo, risponde ai ping e chiude su close.

python
"""ws_echo_server.py - server WebSocket di prova (RFC 6455) per provare ws_client.c.
Fa l'handshake, risponde a ogni messaggio di testo con "eco: <messaggio>", risponde ai ping, chiude su close.
Avvio:  python3 ws_echo_server.py 9001
"""
import base64
import hashlib
import socketserver
import struct
import sys

GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"


def recv_exact(sock, n):
    data = b""
    while len(data) < n:
        chunk = sock.recv(n - len(data))
        if not chunk:
            raise ConnectionError("chiusa")
        data += chunk
    return data


def frame(opcode, payload=b""):
    """Frame del server: FIN=1, NON mascherato."""
    head = bytes([0x80 | opcode])
    n = len(payload)
    if n < 126:
        head += bytes([n])
    elif n < 65536:
        head += bytes([126]) + struct.pack(">H", n)
    else:
        head += bytes([127]) + struct.pack(">Q", n)
    return head + payload


class Handler(socketserver.BaseRequestHandler):
    def handle(self):
        s = self.request
        # --- handshake: la richiesta HTTP arriva fino alla riga vuota ---
        req = b""
        while b"\r\n\r\n" not in req:
            req += s.recv(1024)
        headers = {}
        for line in req.decode("latin-1").split("\r\n")[1:]:
            if ":" in line:
                k, v = line.split(":", 1)
                headers[k.strip().lower()] = v.strip()
        key = headers.get("sec-websocket-key")
        if headers.get("upgrade", "").lower() != "websocket" or not key:
            s.sendall(b"HTTP/1.1 400 Bad Request\r\nContent-Length: 0\r\n\r\n")
            return
        accept = base64.b64encode(hashlib.sha1((key + GUID).encode()).digest()).decode()
        s.sendall(("HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n"
                   "Sec-WebSocket-Accept: %s\r\n\r\n" % accept).encode())
        # --- frame ---
        try:
            while True:
                b0, b1 = recv_exact(s, 2)
                opcode, masked, n = b0 & 0x0F, b1 & 0x80, b1 & 0x7F
                if n == 126:
                    n = struct.unpack(">H", recv_exact(s, 2))[0]
                elif n == 127:
                    n = struct.unpack(">Q", recv_exact(s, 8))[0]
                if not masked:                    # il client DEVE mascherare: errore di protocollo (1002)
                    s.sendall(frame(8, struct.pack(">H", 1002)))
                    return
                mask = recv_exact(s, 4)
                payload = bytes(c ^ mask[i % 4] for i, c in enumerate(recv_exact(s, n)))
                if opcode == 0x1:                 # testo -> eco
                    s.sendall(frame(1, b"eco: " + payload))
                elif opcode == 0x9:               # ping -> pong con lo stesso payload
                    s.sendall(frame(0xA, payload))
                elif opcode == 0x8:               # close -> si risponde con close e si chiude
                    s.sendall(frame(8, payload[:2]))
                    return
        except ConnectionError:
            pass


if __name__ == "__main__":
    socketserver.ThreadingTCPServer.allow_reuse_address = True
    with socketserver.ThreadingTCPServer(("127.0.0.1", int(sys.argv[1])), Handler) as srv:
        srv.serve_forever()

Compilare e provare

$ gcc -Wall -Wextra -o ws_client ws_client.c
$ ./ws_client --test
ws_client: autotest superato
$ python3 ws_echo_server.py 9001 &
$ ./ws_client 127.0.0.1 9001 /echo "ciao"
handshake completato, Sec-WebSocket-Accept = LBdY/uH5+6nEHCyJpwuq8S4gnKA=
ricevuto (9 byte): eco: ciao
connessione chiusa

(Il valore di Sec-WebSocket-Accept cambia a ogni esecuzione perché la chiave è casuale; con la chiave di esempio della RFC varrebbe s3pPLMBiTxaQ9kYGzzhZRbK+xOo=.) Questo client parla solo ws://: i server pubblici di oggi richiedono quasi sempre wss://, cioè TLS sopra TCP, che è oltre l'ambito dell'esercizio. Per vedere i byte scambiati: strace -e trace=write,read -s 200 ./ws_client 127.0.0.1 9001 /echo ciao.

La richiesta inviata dal client:

http
GET /echo HTTP/1.1
Host: 127.0.0.1:9001
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: <24 caratteri Base64>
Sec-WebSocket-Version: 13

e il primo frame (testo ciao, 4 byte, mascherato con una chiave casuale k0 k1 k2 k3):

81 84 k0 k1 k2 k3 (63^k0) (69^k1) (61^k2) (6f^k3)

0x81 = FIN 1 + opcode 1 (testo); 0x84 = MASK 1 + lunghezza 4; poi la chiave; poi i byte di c i a o (63 69 61 6f) in XOR con la chiave, ciclicamente.

Spiegazione dei punti chiave

sha1. L'algoritmo elabora blocchi da 64 byte. Per ogni blocco: espande le 16 parole da 32 bit (lette in big endian) in 80 con w[i] = rol(w[i-3] ^ w[i-8] ^ w[i-14] ^ w[i-16], 1), poi 80 round che aggiornano le cinque parole a, b, c, d, e con una funzione f e una costante k che cambiano ogni 20 round. L'ultimo blocco (o gli ultimi due) è il padding: il resto del messaggio, un byte 0x80, zeri, e la lunghezza del messaggio in bit su 8 byte big endian. Si usa un buffer tail[128]: se il resto è lungo 56 byte o più serve un secondo blocco. Il risultato (20 byte) si serializza in big endian. Per l'handshake la sicurezza di SHA-1 non conta: serve solo a mescolare chiave e GUID (Funzioni hash e crittografia simmetricaLa crittografia trasforma un messaggio in chiaro (plaintext) in un testo cifrato (ciphertext) con una chiave: C = E_ke(P), P = D_kd(C); può dare riservatezza, integrità e autenticazione. Attacchi: solo testo cifrato, testo in chiaro noto, testo in chiaro scelto, forza bruta. Funzione hash: mappa dati di qualsiasi lunghezza in un digest di lunghezza fissa; one-way (dato h è difficile trovare m con hash(m) = h) e resistente alle collisioni; famiglie MD (MD5 rotto per le collisioni nel 2004) e SHA (SHA-0 e SHA-1 rotti, SHA-2 il più usato, SHA-3); costruzione di Merkle-Damgård; usi: integrità e password. Crittografia simmetrica: stessa chiave segreta per cifrare e decifrare; Cesare (E_n(x) = x + n mod 26), Vigenère, Enigma; DES (blocchi da 64 bit, chiave da 56) e AES (blocchi da 128 bit, chiavi da 128, 192, 256); modi ECB (insicuro), CBC, CFB, OFB, CTR. Scambio della chiave con Diffie-Hellman: K = g^(xy) mod p. MAC e HMAC: autenticazione con chiave condivisa.Funzioni hash e crittografia simmetrica →).

base64_encode. Prende 3 byte alla volta in una variabile a 24 bit (v) e ne estrae 4 gruppi da 6 bit con (v >> 18) & 63, (v >> 12) & 63, (v >> 6) & 63, v & 63, ciascuno indice nell'alfabeto A-Za-z0-9+/. Se mancano byte nell'ultimo gruppo i caratteri non significativi diventano =: un byte rimasto → XX==, due → XXX=. Per i 16 byte della chiave: cinque gruppi pieni (15 byte, 20 caratteri) più un byte → 24 caratteri con ==; per i 20 byte di SHA-1: sei gruppi pieni più due byte → 28 caratteri con un =.

ws_accept_value. Concatena la chiave come stringa (24 caratteri, così com'è, senza decodificarla) con il GUID 258EAFA5-E914-47DA-95CA-C5AB0DC85B11, calcola sha1 sui byte della stringa, e codifica i 20 byte binari (non la loro rappresentazione esadecimale) in Base64.

ws_build_frame.

ws_read_frame. Legge 2 byte con read_exact; da h[0] ricava FIN e opcode; da h[1] MASK e la lunghezza a 7 bit; se è 126 o 127 legge 2 o 8 byte e li compone in big endian con plen = plen << 8 | e[i]. Il server non maschera, ma se MASK è acceso legge anche la chiave e smaschera. Controlla che plen <= cap (altrimenti il messaggio è troppo grande per il buffer e il client si ferma: in un client vero si invierebbe close 1009). Legge esattamente plen byte: nessuna interpretazione, perché il payload può contenere qualunque byte.

Il ciclo di ricezione.

  • 0x9 (ping): risponde con 0xA (pong) con lo stesso payload.
  • 0xA (pong): ignorato.
  • 0x8 (close): risponde con un close che ripete i primi 2 byte (il codice) e termina.
  • 0x1, 0x2, 0x0 (testo, binario, continuazione): accoda il payload in message; se FIN è acceso il messaggio è completo. Un messaggio frammentato arriva come un frame con FIN = 0 e opcode di tipo, poi frame con opcode 0 e l'ultimo con FIN = 1: il ciclo li concatena.

La chiusura. ws_send(fd, 0x8, code, 2) con 03 E8 (1000 in big endian: chiusura normale), poi si leggono frame finché arriva il close del server (o la connessione cade) e si chiude il socket.

Errori tipici

  • Frame del client non mascherati: il server chiude con errore di protocollo (1002).
  • Calcolare Accept con la chiave decodificata dal Base64 invece che con la stringa; dimenticare il GUID; codificare in Base64 la stringa esadecimale dello SHA-1 invece dei 20 byte.
  • Dimenticare il padding di SHA-1 o la lunghezza in bit in big endian: l'hash è sbagliato (l'autotest lo scopre).
  • Scrivere la lunghezza 126/127 e dimenticare i byte di lunghezza estesa (o scriverli in little endian).
  • Leggere l'handshake con una read a blocchi: si consumano i primi byte dei frame; qui si legge un byte alla volta.
  • Trattare un messaggio come un solo frame: può essere frammentato; e ignorare i ping (il server chiude per inattività).
  • Stampare il payload con printf("%s") senza lunghezza: non è terminato da '\0' (qui si usa %.*s).
  • Credere che la maschera protegga i dati: serve wss:// (TLS).
  • Usare rand() per la chiave di mascheramento in un client vero: deve essere imprevedibile (qui /dev/urandom, con ripiego solo per le prove).
  • Mascherare i frame dal server, o non controllare Sec-WebSocket-Accept.

Varianti per esercitarsi

  • Leggere il messaggio da tastiera in un ciclo (con poll su stdin e socket) per fare una chat.
  • Inviare un messaggio frammentato (FIN = 0 con testo, poi continuazione) e un ping periodico.
  • Gestire la lunghezza da 16 bit e 64 bit nei frame in ingresso con messaggi da 70000 byte.
  • Scrivere il server WebSocket in C: legge l'handshake, calcola Accept con le stesse funzioni, risponde 101, smaschera i frame del client (Esercizio - Server HTTP iterativo con GET, HEAD e codici di errore (sul modello della prova pratica) fornisce la parte HTTP).
  • Aggiungere il sottoprotocollo (Sec-WebSocket-Protocol: chat).

Versione ripasso

  • Testo. Client ws:// in C senza librerie: handshake (Upgrade, Connection: Upgrade, Sec-WebSocket-Key = 16 byte casuali in Base64, Sec-WebSocket-Version: 13), verifica di 101 e di Sec-WebSocket-Accept; SHA-1 e Base64 a mano con autotest; frame di testo mascherato; lettura frame (ping -> pong, close, frammentazione); chiusura con codice 1000.
  • Accept. Base64( SHA-1( chiave_come_stringa + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" ) ): la chiave non si decodifica; si codificano i 20 byte dello SHA-1. dGhlIHNhbXBsZSBub25jZQ== -> s3pPLMBiTxaQ9kYGzzhZRbK+xOo=.
  • SHA-1. Blocchi da 64 byte; 16 parole big endian espanse a 80 con rol(w[i-3]^w[i-8]^w[i-14]^w[i-16], 1); 80 round su a..e (f e k cambiano ogni 20); padding: byte 0x80, zeri, lunghezza in bit su 8 byte big endian (tail[128]: due blocchi se il resto è >= 56 byte). Autotest SHA-1("abc") = a9993e36....
  • Base64. 3 byte -> 4 indici da 6 bit ((v >> 18) & 63 ...), = di riempimento: Man = TWFu, Ma = TWE=, M = TQ==; 16 byte -> 24 caratteri (==), 20 byte -> 28 (=).
  • ws_build_frame. 0x80 | opcode (FIN = 1); MASK | len con len < 126: 7 bit; 126 + 2 byte; 127 + 8 byte (big endian, con shift); con maschera: 4 byte di chiave e payload[i] ^ key[i % 4]; chiave casuale per frame (/dev/urandom). Esempio RFC: Hello con chiave 37 fa 21 3d = 81 85 37 fa 21 3d 7f 9f 4d 51 58; 300 byte = 82 7e 01 2c ....
  • ws_read_frame. read_exact(2); FIN, opcode, MASK, lunghezza 7 bit; 126/127 -> 2/8 byte composti con plen = plen << 8 | e[i]; chiave solo se MASK; plen <= cap; payload esatto.
  • Ciclo. 0x9 ping -> 0xA pong con lo stesso payload; 0xA ignorato; 0x8 close -> si risponde e si termina; 0x1/0x2/0x0 si accodano fino a FIN. Chiusura: close con 03 E8 poi si attende il close del server.
  • Prove. ./ws_client --test -> autotest superato; python3 ws_echo_server.py 9001; ./ws_client 127.0.0.1 9001 /echo "ciao" -> handshake completato ..., ricevuto (9 byte): eco: ciao, connessione chiusa.
  • Codice essenziale (le funzioni centrali, senza commenti):
c
static void sha1(const uint8_t *msg, size_t len, uint8_t out[20])
{
    uint32_t h[5] = {0x67452301, 0xEFCDAB89, 0x98BADCFE, 0x10325476, 0xC3D2E1F0};
    size_t full = len / 64;
    for (size_t i = 0; i < full; i++)
        sha1_block(h, msg + 64 * i);
    uint8_t tail[128];
    size_t rem = len - full * 64;
    memcpy(tail, msg + full * 64, rem);
    tail[rem] = 0x80;
    size_t tlen = (rem < 56) ? 64 : 128;
    memset(tail + rem + 1, 0, tlen - rem - 1);
    uint64_t bits = (uint64_t)len * 8;
    for (int i = 0; i < 8; i++)
        tail[tlen - 1 - i] = (uint8_t)(bits >> (8 * i));
    sha1_block(h, tail);
    if (tlen == 128)
        sha1_block(h, tail + 64);
    for (int i = 0; i < 5; i++) {
        out[4 * i] = (uint8_t)(h[i] >> 24);
        out[4 * i + 1] = (uint8_t)(h[i] >> 16);
        out[4 * i + 2] = (uint8_t)(h[i] >> 8);
        out[4 * i + 3] = (uint8_t)h[i];
    }
}

static void base64_encode(const uint8_t *in, size_t n, char *out)
{
    static const char T[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
    size_t o = 0;
    for (size_t i = 0; i < n; i += 3) {
        uint32_t v = (uint32_t)in[i] << 16 | (i + 1 < n ? (uint32_t)in[i + 1] << 8 : 0) | (i + 2 < n ? in[i + 2] : 0);
        out[o++] = T[(v >> 18) & 63];
        out[o++] = T[(v >> 12) & 63];
        out[o++] = (i + 1 < n) ? T[(v >> 6) & 63] : '=';
        out[o++] = (i + 2 < n) ? T[v & 63] : '=';
    }
    out[o] = '\0';
}

static size_t ws_build_frame(uint8_t *out, int opcode, const uint8_t *payload, size_t len, const uint8_t *maskkey)
{
    size_t o = 0;
    out[o++] = (uint8_t)(0x80 | opcode);
    uint8_t m = maskkey ? 0x80 : 0;
    if (len < 126) {
        out[o++] = m | (uint8_t)len;
    } else if (len <= 0xFFFF) {
        out[o++] = m | 126;
        out[o++] = (uint8_t)(len >> 8);
        out[o++] = (uint8_t)len;
    } else {
        out[o++] = m | 127;
        for (int i = 7; i >= 0; i--)
            out[o++] = (uint8_t)((uint64_t)len >> (8 * i));
    }
    if (maskkey) {
        memcpy(out + o, maskkey, 4);
        o += 4;
        for (size_t i = 0; i < len; i++)
            out[o + i] = payload[i] ^ maskkey[i % 4];
    } else {
        memcpy(out + o, payload, len);
    }
    return o + len;
}

static int ws_read_frame(int fd, int *opcode, int *fin, uint8_t *payload, size_t cap, size_t *len)
{
    uint8_t h[2];
    if (read_exact(fd, h, 2) < 0)
        return -1;
    *fin = (h[0] & 0x80) != 0;
    *opcode = h[0] & 0x0F;
    int masked = (h[1] & 0x80) != 0;
    uint64_t plen = h[1] & 0x7F;
    if (plen == 126) {
        uint8_t e[2];
        if (read_exact(fd, e, 2) < 0)
            return -1;
        plen = (uint64_t)e[0] << 8 | e[1];
    } else if (plen == 127) {
        uint8_t e[8];
        if (read_exact(fd, e, 8) < 0)
            return -1;
        plen = 0;
        for (int i = 0; i < 8; i++)
            plen = plen << 8 | e[i];
    }
    uint8_t mask[4] = {0, 0, 0, 0};
    if (masked && read_exact(fd, mask, 4) < 0)
        return -1;
    if (plen > cap)
        return -1;
    if (read_exact(fd, payload, (size_t)plen) < 0)
        return -1;
    if (masked)
        for (size_t i = 0; i < plen; i++)
            payload[i] ^= mask[i % 4];
    *len = (size_t)plen;
    return 0;
}
c
static void ws_accept_value(const char *key, char *out )
{
    char cat[128];
    int n = snprintf(cat, sizeof cat, "%s%s", key, WS_GUID);
    uint8_t digest[20];
    sha1((const uint8_t *)cat, (size_t)n, digest);
    base64_encode(digest, 20, out);
}

static int ws_send(int fd, int opcode, const uint8_t *payload, size_t len)
{
    static uint8_t frame[MAX_PAYLOAD + 16];
    if (len > MAX_PAYLOAD)
        return -1;
    uint8_t key[4];
    random_bytes(key, 4);
    size_t n = ws_build_frame(frame, opcode, payload, len, key);
    return write_all(fd, frame, n);
}
  • Errori tipici: frame del client non mascherati; Accept con chiave decodificata o senza GUID o con l'esadecimale; padding di SHA-1 sbagliato; lunghezza estesa omessa o in little endian; handshake letto a blocchi; messaggio frammentato e ping ignorati; %s sul payload; maschera creduta cifratura; rand() per la chiave.

Esercizi su questo argomento

Teoria collegata