Esercizio - Servizio REST con JSON (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 servizio web REST che gestisce in memoria un elenco di compiti (task), ciascuno con id, title e done, rappresentati in JSON:
| operazione | richiesta | risposta di successo |
|---|---|---|
| elencare | GET /tasks |
200, array JSON |
| creare | POST /tasks con {"title":"..."} |
201 Created con Location: /tasks/ID e il compito in JSON |
| leggere | GET /tasks/ID |
200 oppure 404 |
| sostituire | PUT /tasks/ID con {"title":"...","done":true} |
200 oppure 404 |
| eliminare | DELETE /tasks/ID |
204 No Content oppure 404 |
| metodi permessi | OPTIONS |
204 con l'header Allow |
Gli errori sono oggetti JSON {"error":"..."} con i codici giusti: 400 (JSON o campi non validi), 404, 405 (con Allow), 411 (manca Content-Length), 413 (corpo troppo grande), 415 (Content-Type non application/json), 507 (archivio pieno). Il programma deve leggere il corpo con Content-Length, estrarre i campi dal JSON gestendo le sequenze di escape e generare JSON con l'escape corretto.
Teoria: Web service, XML, JSON, SOAP e RESTUn web service e' un servizio software accessibile via rete da altri programmi, con messaggi in formato standard su HTTP; i formati di dati sono XML (marcatori annidati, attributi, namespace, validazione con XSD) e JSON (RFC 8259: oggetti, array, stringhe, numeri, true, false, null; piu' compatto e diretto per i linguaggi di programmazione); SOAP e' un protocollo di messaggi XML (Envelope con Header facoltativo e Body, Fault per gli errori) inviati di solito con una POST HTTP e descritti da un file WSDL; REST e' uno stile architetturale (Fielding) in cui il servizio espone risorse identificate da URI e le manipola con i metodi HTTP: GET legge, POST crea, PUT sostituisce, PATCH modifica, DELETE elimina, con codici di stato significativi (200, 201 con Location, 204, 400, 404, 409), senza stato sul server, con cache e interfaccia uniforme.Web service, XML, JSON, SOAP e REST →, 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 →.
Idea
- Instradamento: dal request-target e dal metodo si ricava l'operazione.
/tasksè la collezione e/tasks/IDun elemento; il metodo dice cosa farne. - Corpo con
Content-Length:POSTePUTportano un corpo JSON da leggere esattamente (rb_readn), dopo aver controllatoContent-Length(411/413) eContent-Type(415). - JSON: estrarre
titleedonedal corpo, e produrre l'uscita con le stringhe correttamente escapate.
Le risposte seguono le convenzioni REST: la creazione risponde 201 e dice dove vive la nuova risorsa con Location; l'eliminazione risponde 204 senza corpo né Content-Type; gli errori sono JSON con un messaggio. Lo stato è un array statico di 64 compiti (used, id, done, title) con next_id che cresce: i dati si perdono alla chiusura del programma.
JSON in C. Non c'è una libreria standard. Le due funzioni di estrazione, json_get_string e json_get_bool, sono volutamente minime: cercano la chiave con strstr e leggono il valore. Per oggetti piatti come questi bastano, ma non validano il documento intero (per un parser vero si usano cJSON o jansson). json_get_string gestisce le sequenze di escape (\", \\, \/, \n, \t) e rifiuta i caratteri di controllo non escapati e le sequenze non gestite (\uXXXX); json_quote fa l'operazione inversa per l'uscita.
Codice
/* rest_server.c - piccolo servizio REST in C: risorsa /tasks (elenco di compiti) in memoria, rappresentata in JSON.
* GET /tasks -> 200, array JSON POST /tasks -> 201 + Location, corpo JSON {"title":"..."}
* GET /tasks/ID -> 200 o 404 PUT /tasks/ID -> 200 o 404, corpo {"title":"...","done":true}
* DELETE /tasks/ID -> 204 o 404 OPTIONS -> 204 + Allow
*
* Compilare: gcc -Wall -Wextra -o rest_server rest_server.c
* Avviare: ./rest_server 8080
* Provare: curl -i -X POST -H 'Content-Type: application/json' -d '{"title":"Comprare il latte"}' http://127.0.0.1:8080/tasks
* curl -i http://127.0.0.1:8080/tasks curl -i http://127.0.0.1:8080/tasks/1
* curl -i -X PUT -H 'Content-Type: application/json' -d '{"title":"Latte","done":true}' http://127.0.0.1:8080/tasks/1
* curl -i -X DELETE http://127.0.0.1:8080/tasks/1
*/
#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>
#define MAX_TASKS 64
#define MAX_TITLE 128
#define MAX_LINE 2048
#define MAX_BODY 4096
struct task {
int used, id, done;
char title[MAX_TITLE];
};
static struct task tasks[MAX_TASKS];
static int next_id = 1;
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;
}
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;
}
}
static int rb_readn(struct rbuf *r, char *dst, size_t n)
{
while (n > 0) {
if (rb_fill(r) < 0)
return -1;
size_t take = r->len - r->pos < n ? r->len - r->pos : n;
memcpy(dst, r->buf + r->pos, take);
r->pos += take;
dst += take;
n -= take;
}
return 0;
}
/* ---------- JSON minimo: oggetti piatti con stringhe e booleani ---------- */
/* Cerca "key" in un oggetto JSON e copia il valore stringa (con le sequenze \" \\ \n \t \/ ) in out.
* Torna 1 se trovato, 0 se manca la chiave, -1 se il valore non e' una stringa valida. */
static int json_get_string(const char *json, const char *key, char *out, size_t size)
{
char pat[64];
snprintf(pat, sizeof pat, "\"%s\"", key);
const char *p = strstr(json, pat);
if (p == NULL)
return 0;
p += strlen(pat);
while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')
p++;
if (*p != ':')
return -1;
p++;
while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')
p++;
if (*p != '"')
return -1;
p++;
size_t o = 0;
while (*p != '"') {
if (*p == '\0' || (unsigned char)*p < 0x20)
return -1; /* stringa non chiusa o carattere di controllo non escapato */
char c = *p++;
if (c == '\\') {
switch (*p++) {
case '"': c = '"'; break;
case '\\': c = '\\'; break;
case '/': c = '/'; break;
case 'n': c = '\n'; break;
case 't': c = '\t'; break;
default: return -1; /* \uXXXX e altre sequenze: non gestite in questo esempio */
}
}
if (o + 1 >= size)
return -1;
out[o++] = c;
}
out[o] = '\0';
return 1;
}
/* Valore booleano: torna 1 se la chiave e' true, 0 se false, -1 se assente o non valido. */
static int json_get_bool(const char *json, const char *key)
{
char pat[64];
snprintf(pat, sizeof pat, "\"%s\"", key);
const char *p = strstr(json, pat);
if (p == NULL)
return -1;
p += strlen(pat);
while (*p == ' ' || *p == ':' || *p == '\t')
p++;
if (strncmp(p, "true", 4) == 0)
return 1;
if (strncmp(p, "false", 5) == 0)
return 0;
return -1;
}
/* Scrive una stringa JSON (con le virgolette) in out, facendo l'escape di ", \ e dei caratteri di controllo. */
static size_t json_quote(const char *s, char *out, size_t size)
{
size_t o = 0;
if (o + 1 < size)
out[o++] = '"';
for (; *s && o + 7 < size; s++) {
unsigned char c = (unsigned char)*s;
if (c == '"' || c == '\\') {
out[o++] = '\\';
out[o++] = (char)c;
} else if (c < 0x20) {
o += (size_t)snprintf(out + o, size - o, "\\u%04x", c);
} else {
out[o++] = (char)c;
}
}
if (o + 1 < size)
out[o++] = '"';
out[o] = '\0';
return o;
}
static int task_json(const struct task *t, char *out, size_t size)
{
char q[2 * MAX_TITLE + 16];
json_quote(t->title, q, sizeof q);
return snprintf(out, size, "{\"id\":%d,\"title\":%s,\"done\":%s}", t->id, q, t->done ? "true" : "false");
}
/* ---------- risposta ---------- */
static const char *reason_of(int code)
{
switch (code) {
case 200: return "OK";
case 201: return "Created";
case 204: return "No Content";
case 400: return "Bad Request";
case 404: return "Not Found";
case 405: return "Method Not Allowed";
case 411: return "Length Required";
case 413: return "Content Too Large";
case 415: return "Unsupported Media Type";
case 507: return "Insufficient Storage";
default: return "Error";
}
}
static void respond(int fd, int code, const char *extra, const char *json)
{
char head[512];
size_t blen = json ? strlen(json) : 0;
int n = snprintf(head, sizeof head, "HTTP/1.1 %d %s\r\n", code, reason_of(code));
if (code != 204) /* 204: niente corpo e niente Content-Type */
n += snprintf(head + n, sizeof head - (size_t)n, "Content-Type: application/json\r\nContent-Length: %zu\r\n", blen);
n += snprintf(head + n, sizeof head - (size_t)n, "%sConnection: close\r\n\r\n", extra);
write_all(fd, head, (size_t)n);
if (code != 204 && blen > 0)
write_all(fd, json, blen);
}
static void respond_error(int fd, int code, const char *extra, const char *msg)
{
char body[256];
snprintf(body, sizeof body, "{\"error\":\"%s\"}", msg); /* msg e' una costante senza virgolette */
respond(fd, code, extra, body);
}
static struct task *find_task(int id)
{
for (int i = 0; i < MAX_TASKS; i++)
if (tasks[i].used && tasks[i].id == id)
return &tasks[i];
return NULL;
}
static void handle(int fd)
{
struct rbuf in = {.fd = fd};
char line[MAX_LINE], method[16], target[MAX_LINE], ver[16];
if (rb_line(&in, line, sizeof line) < 0 || sscanf(line, "%15s %2047s %15s", method, target, ver) != 3 ||
strncmp(ver, "HTTP/1.", 7) != 0) {
respond_error(fd, 400, "", "richiesta non valida");
return;
}
long clen = -1;
int is_json = 0, chunked = 0;
for (;;) {
int len = rb_line(&in, line, sizeof line);
if (len < 0) {
respond_error(fd, 400, "", "richiesta non valida");
return;
}
if (len == 0)
break;
char *colon = strchr(line, ':');
if (colon == NULL)
continue;
*colon = '\0';
const char *v = colon + 1;
while (*v == ' ' || *v == '\t')
v++;
if (strcasecmp(line, "Content-Length") == 0)
clen = atol(v);
else if (strcasecmp(line, "Content-Type") == 0)
is_json = strncasecmp(v, "application/json", 16) == 0;
else if (strcasecmp(line, "Transfer-Encoding") == 0)
chunked = 1;
}
/* riconoscimento della risorsa: /tasks oppure /tasks/ID */
char *q = strchr(target, '?');
if (q)
*q = '\0';
int id = -1, is_collection = 0;
if (strcmp(target, "/tasks") == 0 || strcmp(target, "/tasks/") == 0) {
is_collection = 1;
} else if (strncmp(target, "/tasks/", 7) == 0) {
char *end;
long v = strtol(target + 7, &end, 10);
if (*end != '\0' || end == target + 7 || v < 1 || v > 1000000) {
respond_error(fd, 404, "", "risorsa inesistente");
return;
}
id = (int)v;
} else {
respond_error(fd, 404, "", "risorsa inesistente");
return;
}
/* corpo (solo per POST e PUT) */
char body[MAX_BODY + 1] = "";
int has_body_method = strcmp(method, "POST") == 0 || strcmp(method, "PUT") == 0;
if (has_body_method) {
if (chunked || clen < 0) {
respond_error(fd, 411, "", "serve Content-Length");
return;
}
if (clen > MAX_BODY) {
respond_error(fd, 413, "", "corpo troppo grande");
return;
}
if (!is_json) {
respond_error(fd, 415, "", "serve Content-Type application/json");
return;
}
if (rb_readn(&in, body, (size_t)clen) < 0) {
respond_error(fd, 400, "", "corpo incompleto");
return;
}
body[clen] = '\0';
}
char out[8192], one[512];
if (strcmp(method, "OPTIONS") == 0) {
respond(fd, 204, is_collection ? "Allow: GET, POST, OPTIONS\r\n" : "Allow: GET, PUT, DELETE, OPTIONS\r\n", NULL);
} else if (is_collection && strcmp(method, "GET") == 0) {
size_t o = (size_t)snprintf(out, sizeof out, "[");
int first = 1;
for (int i = 0; i < MAX_TASKS && o + 600 < sizeof out; i++)
if (tasks[i].used) {
o += (size_t)snprintf(out + o, sizeof out - o, "%s", first ? "" : ",");
o += (size_t)task_json(&tasks[i], out + o, sizeof out - o);
first = 0;
}
snprintf(out + o, sizeof out - o, "]");
respond(fd, 200, "", out);
} else if (is_collection && strcmp(method, "POST") == 0) {
char title[MAX_TITLE];
int rc = json_get_string(body, "title", title, sizeof title);
if (rc != 1 || title[0] == '\0') {
respond_error(fd, 400, "", "serve un campo title (stringa non vuota)");
return;
}
struct task *slot = NULL;
for (int i = 0; i < MAX_TASKS && slot == NULL; i++)
if (!tasks[i].used)
slot = &tasks[i];
if (slot == NULL) {
respond_error(fd, 507, "", "archivio pieno");
return;
}
slot->used = 1;
slot->id = next_id++;
slot->done = 0;
snprintf(slot->title, sizeof slot->title, "%s", title);
task_json(slot, one, sizeof one);
char extra[64];
snprintf(extra, sizeof extra, "Location: /tasks/%d\r\n", slot->id); /* dove vive la nuova risorsa */
respond(fd, 201, extra, one);
} else if (!is_collection && strcmp(method, "GET") == 0) {
struct task *t = find_task(id);
if (t == NULL) {
respond_error(fd, 404, "", "compito inesistente");
return;
}
task_json(t, one, sizeof one);
respond(fd, 200, "", one);
} else if (!is_collection && strcmp(method, "PUT") == 0) {
struct task *t = find_task(id);
if (t == NULL) {
respond_error(fd, 404, "", "compito inesistente");
return;
}
char title[MAX_TITLE];
int rc = json_get_string(body, "title", title, sizeof title);
int done = json_get_bool(body, "done");
if (rc != 1 || title[0] == '\0' || done < 0) { /* PUT sostituisce TUTTA la risorsa: servono entrambi i campi */
respond_error(fd, 400, "", "servono title e done");
return;
}
snprintf(t->title, sizeof t->title, "%s", title);
t->done = done;
task_json(t, one, sizeof one);
respond(fd, 200, "", one);
} else if (!is_collection && strcmp(method, "DELETE") == 0) {
struct task *t = find_task(id);
if (t == NULL) {
respond_error(fd, 404, "", "compito inesistente");
return;
}
t->used = 0;
respond(fd, 204, "", NULL);
} else {
respond_error(fd, 405, is_collection ? "Allow: GET, POST, OPTIONS\r\n" : "Allow: GET, PUT, DELETE, OPTIONS\r\n",
"metodo non permesso");
}
fprintf(stderr, "%s %s\n", method, target);
}
int main(int argc, char **argv)
{
if (argc != 2) {
fprintf(stderr, "uso: %s porta\n", argv[0]);
return 1;
}
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;
}
for (;;) {
int cfd = accept(lfd, NULL, NULL);
if (cfd < 0) {
if (errno == EINTR)
continue;
perror("accept");
break;
}
handle(cfd);
close(cfd);
}
return 0;
}Compilare e provare
$ gcc -Wall -Wextra -o rest_server rest_server.c
$ ./rest_server 8080 &Una sessione completa con curl (-i mostra gli header; -d invia un corpo e imposta Content-Type: application/x-www-form-urlencoded, quindi per il JSON si specifica l'header):
$ curl -i -X POST -H 'Content-Type: application/json' -d '{"title":"Comprare il latte"}' http://127.0.0.1:8080/tasks
HTTP/1.1 201 Created
Content-Type: application/json
Content-Length: 49
Location: /tasks/1
Connection: close
{"id":1,"title":"Comprare il latte","done":false}
$ curl -i http://127.0.0.1:8080/tasks
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 51
[{"id":1,"title":"Comprare il latte","done":false}]
$ curl -i http://127.0.0.1:8080/tasks/9
HTTP/1.1 404 Not Found
Content-Length: 31
{"error":"compito inesistente"}
$ curl -i -X PUT -H 'Content-Type: application/json' -d '{"title":"Latte","done":true}' http://127.0.0.1:8080/tasks/1
HTTP/1.1 200 OK
Content-Length: 36
{"id":1,"title":"Latte","done":true}
$ curl -i -X DELETE http://127.0.0.1:8080/tasks/1
HTTP/1.1 204 No Content
Connection: close
$ curl -i -X OPTIONS http://127.0.0.1:8080/tasks
HTTP/1.1 204 No Content
Allow: GET, POST, OPTIONSGli errori (le risposte sono quelle del programma):
$ curl -i -X PUT -H 'Content-Type: application/json' -d '{"title":"Latte"}' http://127.0.0.1:8080/tasks/1 # manca done
HTTP/1.1 400 Bad Request ... {"error":"servono title e done"}
$ curl -i -X POST -H 'Content-Type: application/json' -d '{"nome":"x"}' http://127.0.0.1:8080/tasks # manca title
HTTP/1.1 400 Bad Request ... {"error":"serve un campo title (stringa non vuota)"}
$ curl -i -X POST -H 'Content-Type: text/plain' -d '{"title":"x"}' http://127.0.0.1:8080/tasks # tipo sbagliato
HTTP/1.1 415 Unsupported Media Type ... {"error":"serve Content-Type application/json"}
$ curl -i -X PATCH http://127.0.0.1:8080/tasks/2 # metodo non permesso
HTTP/1.1 405 Method Not Allowed
Allow: GET, PUT, DELETE, OPTIONS
$ curl -i http://127.0.0.1:8080/altro # risorsa inesistente
HTTP/1.1 404 Not Found ... {"error":"risorsa inesistente"}Un titolo con virgolette e a-capo ('{"title":"Dire \"ciao\"\n e \\ ok"}') viene letto correttamente e restituito con l'escape: {"id":1,"title":"Dire \"ciao\"\u000a e \\ ok","done":false} (il carattere di controllo \n esce come \u000a, valido in JSON).
Spiegazione dei punti chiave
handle: lettura della richiesta. Come negli altri server: request line con sscanf e controllo della versione (400), header con rb_line; si memorizzano Content-Length, Content-Type (vero se inizia per application/json, senza distinguere maiuscole) e la presenza di Transfer-Encoding.
Instradamento. Si toglie la query (?...). strcmp(target, "/tasks") (o con / finale) è la collezione; /tasks/ seguito da un numero è un elemento: strtol(target + 7, &end, 10) con controllo *end != '\0' (il percorso deve finire subito dopo il numero) e intervallo valido; altro → 404. Il metodo viene dopo: GET, POST, PUT, DELETE, OPTIONS; per ogni altra combinazione 405 con l'Allow della risorsa.
Il corpo di POST e PUT (ordine dei controlli).
chunkedoContent-Lengthassente →411 Length Required;Content-Length > MAX_BODY(4096) →413;Content-Typediverso daapplication/json→415;rb_readnlegge esattamenteContent-Lengthbyte (anche quelli già nel buffer); se la connessione cade →400; si termina il buffer con'\0'.
Il limite sulla dimensione è fondamentale: senza, un Content-Length enorme farebbe allocare memoria o attendere all'infinito.
json_get_string. Cerca "chiave" con strstr, salta gli spazi, richiede :, salta gli spazi, richiede ", poi copia carattere per carattere fino alla " finale:
- un
\introduce una sequenza di escape:\"e\\e\/e\ne\tsi convertono, qualunque altra (per esempioè) fa fallire la funzione (limite dichiarato); - un carattere sotto
0x20non escapato, o la fine della stringa C prima della", → errore (JSON non valido); - se il valore non entra nel buffer (
o + 1 >= size) → errore, non troncamento silenzioso.
Restituisce 1 se trovato, 0 se la chiave manca, -1 se il valore non è una stringa valida. Limite: strstr non conosce la struttura: {"nota":"il title è qui","title":"x"} troverebbe "title" dentro il valore di nota. Per dati non fidati serve un parser vero.
json_get_bool cerca true o false dopo i due punti; per PUT (che sostituisce l'intera risorsa) servono entrambi i campi: se manca uno → 400.
json_quote. Scrive la stringa fra virgolette facendo l'escape di " e \ e dei caratteri di controllo (\u00XX con %04x): è ciò che rende valido il JSON generato. Dimenticarlo permette di rompere la struttura (o iniettare campi) con un titolo come x","done":true,"y":".
respond. Compone la status-line con reason_of; per ogni codice diverso da 204 aggiunge Content-Type: application/json e Content-Length (la lunghezza del corpo, che è sempre nota: niente chunked); gli header supplementari (Location, Allow) vengono da extra, già terminati da \r\n. Per 204 non c'è né corpo né Content-Type né Content-Length: la risposta finisce con la riga vuota.
Le operazioni.
POST: estraetitle(non vuoto) →400altrimenti; cerca uno slot libero (507se l'archivio è pieno); assegnaid = next_id++,done = 0; risponde201conLocation: /tasks/IDe il JSON del nuovo compito. L'identificatore lo sceglie il server.GET /tasks: costruisce l'array scorrendo gli slot usati, con la virgola fra gli elementi.GET /tasks/ID,PUT,DELETE:find_task(id);404se non esiste.DELETEazzerausede risponde204; ripetuta, dà404(l'effetto sul server è lo stesso: idempotenza).
Dove sta REST. Gli URI indicano nomi (/tasks/1, non /leggiCompito?id=1); i metodi hanno il loro significato (GET sicuro e idempotente, PUT idempotente, POST non idempotente); i codici di stato comunicano l'esito (201 + Location, 204, 404, 415); non c'è stato di sessione sul server (ogni richiesta è completa).
Errori tipici
- Rispondere
200per tutto, anche per gli errori, con l'errore nel corpo. POSTche risponde200senzaLocation, oDELETEche risponde200con un corpo vuoto senzaContent-Length; con204non si invia il corpo.- Non controllare
Content-Length: leggere più o meno byte del corpo desincronizza la connessione. - Non limitare la dimensione del corpo.
- Non verificare
Content-Type, e interpretare come JSON un corpo qualsiasi. - Generare JSON concatenando stringhe senza escape (injection nel JSON, JSON non valido).
- Interpretare JSON con
strstrsu dati non fidati (confusione fra chiavi e valori). - Usare
PUTper modifiche parziali: sostituisce tutta la risorsa. - Verbi negli URI (
/creaCompito) o metodi usati male (GETche modifica dati). - Buffer fissi senza controllo di lunghezza nelle copie di
title.
Varianti per esercitarsi
- Concorrenza con
fork: lo stato in memoria non si condivide fra processi, quindi serve un file o memoria condivisa (Esercizio - Server HTTP concorrente con fork e connessioni persistenti (sul modello della prova pratica)). - Persistenza: salvare i compiti in un file all'avvio e dopo ogni modifica.
PATCH /tasks/IDcon solo i campi da cambiare.- Filtri e paginazione:
GET /tasks?done=true&pagina=2. - Autenticazione Basic sulle operazioni di scrittura (Esercizio - Server con autenticazione Basic, ETag e GET condizionale (sul modello della prova pratica)), e
ETag/If-Matchper evitare gli aggiornamenti persi (412). - Negoziare il formato (
Accept: application/xml).
Versione ripasso
- Testo. Risorsa
/tasksin memoria (id,title,done) in JSON:GET /tasks(200),POST(201+Location: /tasks/ID),GET /tasks/ID(200/404),PUT(200/404, sostituisce tutto),DELETE(204/404),OPTIONS(204+Allow); errori JSON{"error":"..."}:400,404,405+Allow,411,413,415,507. - Instradamento. Query tolta;
/tasks= collezione;/tasks/IDconstrtole*end == '\0'= elemento; altro404; combinazione metodo/risorsa non prevista ->405conAllow(collezione:GET, POST, OPTIONS; elemento:GET, PUT, DELETE, OPTIONS). - Corpo di
POST/PUT. (1)chunkedo nienteContent-Length->411; (2)> MAX_BODY(4096) ->413; (3)Content-Typenonapplication/json->415; (4)rb_readndi esattamenteContent-Lengthbyte, poi'\0'. json_get_string.strstr("\"chiave\""), spazi,:, spazi,"; copia con escape (\" \\ \/ \n \t); altre sequenze (\uXXXX), caratteri di controllo non escapati, stringa non chiusa, buffer pieno -> errore (-1);0se la chiave manca. Non è un parser:strstrpuò trovare la chiave dentro un valore.json_get_bool:true/falsedopo i due punti;PUTrichiedetitleedone.json_quote. Virgolette, escape di",\e dei caratteri sotto0x20(\u%04x): necessario perché il JSON generato sia valido e non iniettabile.respond. Status-line; per ogni codice tranne204:Content-Type: application/jsoneContent-Length;extraperLocationeAllow;204= nessun corpo,Content-TypenéContent-Length.- Operazioni.
POST:titlenon vuoto (400), slot libero (507),id = next_id++,done = 0,201+Location.GETcollezione: array con virgole.PUT: titolo edoneobbligatori.DELETE:used = 0,204; ripetuta404(idempotente). - REST. URI = nomi; metodi con il loro significato (
GETsicuro,PUT/DELETEidempotenti,POSTno); codici di stato come contratto; nessuna sessione sul server. - Prove.
curl -i -X POST -H 'Content-Type: application/json' -d '{"title":"Comprare il latte"}' http://127.0.0.1:8080/tasks->201,Location: /tasks/1, corpo di 49 byte;PUT->200;DELETE->204;-H 'Content-Type: text/plain'->415;PATCH->405; titolo con"e\nrestituito con escape (\u000a). - Codice essenziale (le funzioni centrali, senza commenti):
static int json_get_string(const char *json, const char *key, char *out, size_t size)
{
char pat[64];
snprintf(pat, sizeof pat, "\"%s\"", key);
const char *p = strstr(json, pat);
if (p == NULL)
return 0;
p += strlen(pat);
while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')
p++;
if (*p != ':')
return -1;
p++;
while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')
p++;
if (*p != '"')
return -1;
p++;
size_t o = 0;
while (*p != '"') {
if (*p == '\0' || (unsigned char)*p < 0x20)
return -1;
char c = *p++;
if (c == '\\') {
switch (*p++) {
case '"': c = '"'; break;
case '\\': c = '\\'; break;
case '/': c = '/'; break;
case 'n': c = '\n'; break;
case 't': c = '\t'; break;
default: return -1;
}
}
if (o + 1 >= size)
return -1;
out[o++] = c;
}
out[o] = '\0';
return 1;
}
static size_t json_quote(const char *s, char *out, size_t size)
{
size_t o = 0;
if (o + 1 < size)
out[o++] = '"';
for (; *s && o + 7 < size; s++) {
unsigned char c = (unsigned char)*s;
if (c == '"' || c == '\\') {
out[o++] = '\\';
out[o++] = (char)c;
} else if (c < 0x20) {
o += (size_t)snprintf(out + o, size - o, "\\u%04x", c);
} else {
out[o++] = (char)c;
}
}
if (o + 1 < size)
out[o++] = '"';
out[o] = '\0';
return o;
}
static void respond(int fd, int code, const char *extra, const char *json)
{
char head[512];
size_t blen = json ? strlen(json) : 0;
int n = snprintf(head, sizeof head, "HTTP/1.1 %d %s\r\n", code, reason_of(code));
if (code != 204)
n += snprintf(head + n, sizeof head - (size_t)n, "Content-Type: application/json\r\nContent-Length: %zu\r\n", blen);
n += snprintf(head + n, sizeof head - (size_t)n, "%sConnection: close\r\n\r\n", extra);
write_all(fd, head, (size_t)n);
if (code != 204 && blen > 0)
write_all(fd, json, blen);
}static int task_json(const struct task *t, char *out, size_t size)
{
char q[2 * MAX_TITLE + 16];
json_quote(t->title, q, sizeof q);
return snprintf(out, size, "{\"id\":%d,\"title\":%s,\"done\":%s}", t->id, q, t->done ? "true" : "false");
}
static struct task *find_task(int id)
{
for (int i = 0; i < MAX_TASKS; i++)
if (tasks[i].used && tasks[i].id == id)
return &tasks[i];
return NULL;
}- Errori tipici: sempre
200;POSTsenza201/Location;204con corpo;Content-Lengthnon controllato o corpo senza limite;Content-Typeignorato; JSON generato senza escape;strstrsu dati non fidati;PUTper modifiche parziali; verbi negli URI; stato in memoria creduto condiviso fra processifork.