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://):
- aprire la connessione TCP e fare l'handshake: richiesta
GETconUpgrade: websocket,Connection: Upgrade,Sec-WebSocket-Version: 13eSec-WebSocket-Key(16 byte casuali in Base64); controllare che la risposta sia101e cheSec-WebSocket-AcceptvalgaBase64(SHA-1(chiave + GUID)); - implementare SHA-1 e Base64 e verificarli con i valori della RFC (esempio: chiave
dGhlIHNhbXBsZSBub25jZQ==→s3pPLMBiTxaQ9kYGzzhZRbK+xOo=); - inviare un messaggio di testo in un frame mascherato, con la lunghezza codificata correttamente (7, 16 o 64 bit);
- leggere i frame del server (non mascherati), gestire ping (rispondere con pong), close e messaggi frammentati; stampare il messaggio ricevuto;
- chiudere in modo ordinato con un frame
close(codice 1000) e attendere ilclosedel 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
/* 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.
"""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:
GET /echo HTTP/1.1
Host: 127.0.0.1:9001
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: <24 caratteri Base64>
Sec-WebSocket-Version: 13e 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.
out[0] = 0x80 | opcode: FIN = 1 (messaggio in un solo frame) e opcode1testo,8close,9ping,Apong.- Lunghezza: sotto 126 sta nei 7 bit; da 126 a 65535 si scrive
126e 2 byte in big endian; oltre,127e 8 byte ((len >> 8*i)perida 7 a 0). La lunghezza è un numero a più byte: va in ordine di rete, e nel codice si compone con shift senza dipendere dalla macchina (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 →). - Con
maskkeynon nullo: bitMASKacceso, 4 byte di chiave, poipayload[i] ^ maskkey[i % 4]. L'autotest verifica il frame della RFC (81 85 37 fa 21 3d 7f 9f 4d 51 58perHellocon chiave37 fa 21 3d) e quello da 300 byte (82 7e 01 2c ...). - I frame del client sono sempre mascherati (chiave casuale nuova a ogni frame, da
random_bytes); la maschera protegge gli intermediari, non è cifratura.
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 con0xA(pong) con lo stesso payload.0xA(pong): ignorato.0x8(close): risponde con uncloseche ripete i primi 2 byte (il codice) e termina.0x1,0x2,0x0(testo, binario, continuazione): accoda il payload inmessage; seFINè acceso il messaggio è completo. Un messaggio frammentato arriva come un frame conFIN = 0e opcode di tipo, poi frame con opcode0e l'ultimo conFIN = 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
Acceptcon 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/127e dimenticare i byte di lunghezza estesa (o scriverli in little endian). - Leggere l'handshake con una
reada 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
pollsustdine socket) per fare una chat. - Inviare un messaggio frammentato (
FIN = 0con 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
Acceptcon le stesse funzioni, risponde101, 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 di101e diSec-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 sua..e(fekcambiano ogni 20); padding: byte0x80, zeri, lunghezza in bit su 8 byte big endian (tail[128]: due blocchi se il resto è >= 56 byte). AutotestSHA-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 | lenconlen < 126: 7 bit;126+ 2 byte;127+ 8 byte (big endian, con shift); con maschera: 4 byte di chiave epayload[i] ^ key[i % 4]; chiave casuale per frame (/dev/urandom). Esempio RFC:Hellocon chiave37 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 conplen = plen << 8 | e[i]; chiave solo seMASK;plen <= cap; payload esatto.- Ciclo.
0x9ping ->0xApong con lo stesso payload;0xAignorato;0x8close -> si risponde e si termina;0x1/0x2/0x0si accodano fino aFIN. Chiusura:closecon03 E8poi si attende ilclosedel 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):
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;
}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;
Acceptcon 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;%ssul payload; maschera creduta cifratura;rand()per la chiave.